commit 086d658c622388471b92a1bc79e23dc0f85a059c Author: zc Date: Thu Feb 5 18:01:33 2026 +0800 first commit diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000..628717c --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,7 @@ +--- +exclude_paths: + - '.mvn/**' + - '**.md' + - '**/**.md' + - 'mica-mqtt-example/**' + - '**/test/**' diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8cfd370 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# http://editorconfig.org +root = true + +# 空格替代Tab缩进在各种编辑工具下效果一致 +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[*.java] +indent_style = tab + +[*.{json,yml}] +indent_size = 2 + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8e13b69 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# All text files should have the "lf" (Unix) line endings +* text eol=lf +# windows cmd shoud have the "crlf" (Win32) line endings +*.cmd eol=crlf + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.java text +*.js text +*.css text +*.html text +*.properties text +*.xml text +*.yml text + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.ttf binary +*.jar binary +*.db binary +*.xdb binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34f8219 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +### gradle ### +.gradle +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +!.idea/icon.png +.idea +out/ +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +### vscode ### +.vscode + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz + +# logs # +logs + +# temp ignore +*.log +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +# system ignore +.DS_Store +Thumbs.db +Servers +.metadata +upload +gen_code + +# Flattened pom +.flattened-pom.xml +/**/.flattened-pom.xml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..65978d0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,625 @@ +# 变更记录 + +## 发行版本 + +### v2.5.9 - 2025-11-29 + +- :sparkles: mica-mqtt-client solon 和 spring 插件 MQTT 客户端订阅中的 beanName 支持占位符解析,感谢 `@tan90` 反馈(gitee #ID7PF6) +- :sparkles: mica-mqtt-server ClientInfo 添加 SSL 和 WebSocket 标识。 +- :arrow_up: 升级到 mica-net 1.2.4,优化 sse,修复 jackson3 方法错误。 + +### v2.5.7 - 2025-11-07 + +- :sparkles: mica-mqtt-server 新增 `/api/v1/stats/sse` 接口,支持通过 SSE 实时获取服务器统计信息 +- :sparkles: example 升级到 solon 3.7.0 更改相关依赖命名规则 +- :arrow_up: 升级 mica-net 到 1.2.2 支持 snack4 json 序列化,内存优化和消息发送性能优化 +- :bug: mica-mqtt-server-solon-plugin 移除 MqttServerConfiguration bean MqttFunctionManager 的 static 修饰符 + +### v2.5.6 - 2025-10-27 +- :bug: 修复 MQTT 解码器中的缓冲区读取问题,修复解码异常重连后无法恢复的问题。(所有版本) +- :arrow_up: 升级 mica-net 到 1.2.1,修改慢包读取 (gitee #ID3IAU),影响范围(2.5.5) + +### v2.5.5 - 2025-10-10 +- :sparkles: mqtt-client 添加通过 `Consumer` 函数式接口自定义遗嘱属性 +- :sparkles: mqtt-client 添加直接使用 MqttPublishBuilder 发布消息 +- :sparkles: mqtt-client 添加 disconnectBeforeStop 配置(默认 true),断开连接前是否发送 disconnect 消息,感谢 `@steven` 反馈(gitee #ICXY4A) +- :sparkles: mica-mqtt-server 使用 ConcurrentHashMap 替代 IntObjectHashMap,优化内存会话管理 +- :sparkles: mica-mqtt-server-spring-boot-starter bean 加载顺序优化,避免出现提示 +- :arrow_up: 升级 mica-net 到 1.2.0,调整慢包攻击规则和支持 jackson3,感谢 `@well` 反馈(gitee #ICXF5N) + +### v2.5.4 - 2025-08-29 +- :sparkles: mica-mqtt-server 使用前缀树管理 MQTT 订阅。 +- :sparkles: mica-mqtt-server 心跳超时小于等于0时,不开启心跳检测。(不建议这样使用)感谢 `@刘业兴` 反馈(gitee #ICTT2V) +- :sparkles: mica-mqtt-server solon 和 spring 插件,将 `@MqttServerFunction` 统一到 mica-mqtt-common 包中,不兼容。 +- :sparkles: mica-mqtt-server solon 和 spring 插件,`@MqttServerFunction` 增加 topic 变量解析功能,支持解析 Map 类型的 topic 中的 ${topicVars} 变量参数。 +- :sparkles: mica-mqtt-client solon 和 spring 插件,将 `@MqttClientSubscribe` 统一到 mica-mqtt-common 包中,MqttClientTemplate 中的 `DEFAULT_CLIENT_TEMPLATE_BEAN` 常量定义移动到了 `@MqttClientSubscribe`,不兼容。 +- :sparkles: mica-mqtt-client solon 和 spring 插件,`@MqttClientSubscribe` 注解订阅,增加 topic 变量解析功能,支持解析 Map 类型的 topic 中的 ${topicVars} 变量参数。 +- :sparkles: mica-mqtt-codec 移除了 MqttCodecUtil 中的 `isValidPublishTopicName` 方法 ,直接使用 `isTopicFilter` 校验发布主题名称是否包含通配符。 +- :sparkles: mica-mqtt-codec 包调整,重命名类名、方法名,重构 MQTT 消息构建器类(为后续方便做准备),不兼容。 +- :sparkles: mica-mqtt-common `TopicUtil` 调整 validateTopicFilter 方法,移除了对空白字符的校验。(注意:emqx 支持使用空白字符,mosquitto 不支持)。 +- :sparkles: `TopicUtil` 和 `MqttCodecUtil` 增加对 topic 中空白符的日志提示。感谢 `@长草颜团子` 反馈(gitee #26) + +### v2.5.3 - 2025-08-03 +- :sparkles: mica-mqtt-server-spring-boot-starter 支持注解 `@MqttServerFunction` 监听 +- :sparkles: mica-mqtt-server-solon-plugin 支持注解 `@MqttServerFunction` 监听 +- :sparkles: mica-mqtt-client-solon-plugin 更新 `solon-configuration-metadata.json` +- :sparkles: mica-mqtt-codec `ReasonCode` 统一移动到 `codes` 包(不影响老用户升级) + +### v2.5.2 - 2025-07-27 +- :bug: mica-mqtt-server 修复启动报错(影响范围 `2.5.0` ~ `2.5.1`),感谢 `CoderKK` 反馈(gitee #ICOQ3Q) + +### v2.5.1 - 2025-07-24 +- :sparkles: mica-mqtt-server 优化 sse mcp,添加 sse 心跳 +- :sparkles: mica-mqtt-client 内置 ssl SNI 支持,感谢 `sword007`、`@TomatoLay` 反馈(gitee #ICKBAY #ICEANP) +- :sparkles: mica-mqtt-client 支持多网卡下指定网卡 `bindIp`(网卡对应IP)和 `bindNetworkInterface`(网卡名) 配置(2个方法使用任意一个即可)。感谢 `@iovera` 反馈(gitee #ICO699) +- :bug: mica-mqtt-client 高CPU下 packetId 生成超限,感谢 `@火焰之魂` 反馈(gitee #ICLXC3) + +### v2.5.0 - 2025-07-12 +- :sparkles: mica-mqtt X AI,mica-mqtt-server 支持大模型 mcp +- :sparkles: mica-mqtt-server 支持同时配置多协议支持,拆分 `http(默认端口18083)` 和 `websocket(默认端口8083)`,使统计更加准确 +- :sparkles: mica-mqtt-server spring、solon 插件删除不推荐使用的 `EventMqttMessageListener` +- :sparkles: mica-mqtt-server 调整保留消息的规则,支持 `$retain` 带存储周期的保留消息 感谢 `@tan90` pr #ICB9I2 #23 +- :sparkles: mica-mqtt-client 删除 `IMqttClientMessageIdGenerator` 接口合入 `IMqttClientSession` 接口 +- :sparkles: mica-mqtt-client 默认 mqtt5.0,cleanSession 改为 cleanStart 感谢 `@tan90` 反馈 (gitee #IBKKAG) +- :sparkles: mica-mqtt-client MqttClient 和 MqttClientTemplate 支持通过代理接口来进行Publish 感谢 `@galaxy-sea` pr (github #100) +- :sparkles: mica-mqtt-codec 完全同步成私服版,将 MqttProperties 内部类拆解出来,方便使用 +- :sparkles: mica-mqtt client、server solon 插件添加对 [Solon IDEA](https://plugins.jetbrains.com/plugin/21380-solon) 插件配置提示支持。 +- :sparkles: 移除 mica-mqtt-broker,未来重构 +- :sparkles: 统一参数命名,userName 统一为 username。 +- :sparkles: 优化部分日志,使用中文,方便大家排查问题 + +### v2.4.9 - 2025-06-27 +- :bug: mqtt server 修复 gitee #ICID15 + +### v2.4.8 - 2025-06-20 +- :sparkles: mica-mqtt-client-spring-boot-starter `MqttClientSubscribeDetector` bean 配置改成 `static` 方法。 +- :sparkles: mica-mqtt-server 调整保留消息标志位的规则。 + +### v2.4.7 - 2025-06-02 +- :sparkles: mica-mqtt-client、mica-mqtt-server publish相关接口支持object发送 (github #98) 感谢 `@galaxy-sea` 贡献 +- :sparkles: mica-mqtt-client 调整 MqttClient#reconnect 策略,(gitee #IBY5LQ)感谢 `@拉风的CC` 反馈。 +- :sparkles: mica-mqtt-codec MqttCodecUtil#isTopicFilter 代码优化改为逆序循环 +- :sparkles: mica-mqtt-codec 代码优化详见: https://github.com/netty/netty/pull/15227 +- :sparkles: mica-net-http http api 响应头 name 不转换成小写 +- :wrench: mica-mqtt-common 更新 `module-info.java` 添加序列化模块 +- :bug: mica-net-utils DefaultThreadFactory 不应该共用。 + +### v2.4.6 - 2025-05-19 +- :sparkles: mica-mqtt-client-spring-boot-starter MqttDeserializer 接口重写,支持泛型调用 (github #95) 感谢 `@galaxy-sea` 贡献 +- :sparkles: mica-mqtt-client 批量订阅兼容 mqtt 3.1.1 部分 broker 只返回一个 reasonCode 的情况。感谢 `@Jacky` 反馈 +- :sparkles: mica-mqtt-server-solon-plugin 添加对 metrics 指标的支持 +- :sparkles: mica-mqtt-client-solon-plugin 注解订阅支持自定义序列化(默认 json 序列化)和泛型 + +### v2.4.5 - 2025-05-06 +- :sparkles: mica-mqtt-client-spring-boot-starter 的 `@MqttClientSubscribe` 注解支持自定义反序列化。 +- :sparkles: 优化代码 Spring Boot Client 可以自定义 MqttClientSubscribeDetector github #90 感谢 `@galaxy-sea` 贡献 +- :sparkles: 升级 mica-net 到 1.1.6,解决 eclipse paho mqtt websocket client 连接 mica-mqtt server 报错 +- :sparkles: 依赖调整,减少 example 示例项目的安全提示 +- :bug: 解决服务端重启时 client有消息发送,导致 client 无法正常重连 gitee #IC4DWT 感谢 `@wtjperi2003` 反馈 +- :bug: 同步 netty mqtt codec Fix the assignment error of maxQoS parameter in ConnAck Properties + +### v2.4.4 - 2025-04-13 +- :sparkles: mica-mqtt-server,更好的兼容 Android 环境。github #81 感谢 `@KittenBall` 的联调测试。 + +### v2.4.3 - 2025-03-23 +- :sparkles: Central Portal 开始支持 Snapshots(仅存储90天,需尽快切到最新的正式版),dev 分支提交后 Github action 自动发布快照版。 +- :sparkles: 精简,删除没有用到的代码,下沉到 mica-net。 +- :sparkles: mica-mqtt-client 添加 heartbeatMode 和 heartbeatTimeoutStrategy,用于某些弱网场景 gitee #IBSMZ7 感谢 `@拉风的CC` 反馈。 +- :sparkles: mica-mqtt-server 默认依赖上 mica-net-http,不再需要手动添加依赖,简化使用。 +- :sparkles: mica-mqtt-server-spring-boot-starter MqttServerTemplate 暴露 `getMqttServer()` 方法,方便使用。 +- :sparkles: mica-mqtt-server-spring-boot-starter 兼容存在 `MeterRegistry` 类,但是 `MeterRegistry` bean 不存在的情况。gitee #IBLBCY 感谢 `@xxg` 反馈。 + +### v2.4.2 - 2025-01-24 +- :sparkles: mica-mqtt-client Spring Boot stater 和 solon 插件添加工作线程数配置 `bizThreadPoolSize` (默认:2,如果消息量大,业务复杂处理慢,例如做emqx消息转发处理,可调大此配置)。 +- :sparkles: mica-mqtt-client Spring Boot stater 和 solon 插件添加 MQTT5.0 的 `sessionExpiryIntervalSecs` 配置 gitee #IBIE27 感谢 `@cyber` 反馈。 +- :sparkles: mica-mqtt-client 调整重连重新订阅逻辑,Spring Boot stater 和 solon 插件 `@MqttClientSubscribe` 注解订阅,保留 session 重连时不丢失消息 gitee #IBIE27 感谢 `@cyber` 反馈。 +- :sparkles: mica-mqtt-client DefaultMqttClientSession 移除 `final` 修饰,方便继承自定义。 +- :sparkles: mica-mqtt-client 将 clientId 绑定到 context 上,可以使用 `context.getId()` 获取,方便多 mqtt client 实例下使用,gitee #IBHHB1 感谢 `@cv` 反馈。 +- :sparkles: mica-mqtt-server proxy 代理协议简化,已测底抽象到 mica-net。 +- :sparkles: mica-mqtt-common 调整 `TopicUtil`,支持原生 Android,gitee #IBJBFL 感谢 `@DeanNode` 反馈。 +- :sparkles: mica-mqtt-server 默认的 nodeName 改为随机 `nanoId`,支持原生 Android,gitee #IBJBFL 感谢 `@DeanNode` 反馈。 +- :sparkles: 将 MqttServerCustomizer 和 MqttClientCustomizer 抽到 mica-mqtt-server、mica-mqtt-client 方便组件封装,需要调整包名,请先将老的包导入删除,idea 会自动引入新的包。 +- :bug: mica-mqtt-client-spring-boot-starter 修复 Spring Boot 3.2 启动时出现警告 gitee #IBITP5 感谢 `@cyber` 反馈。 + +### v2.4.2-M2 - 2025-01-23 +- :sparkles: mica-mqtt-client Spring boot stater 和 solon 添加工作线程数配置 `bizThreadPoolSize` 。 +- :sparkles: mica-mqtt-server proxy 代理简化,已测底抽象到 mica-net。 +- :sparkles: mica-mqtt-common 调整 TopicUtil,支持原生 Android,gitee #IBJBFL 感谢 `@DeanNode` 反馈。 +- :sparkles: mica-mqtt-server nodeName 改为随机 nanoId ,支持原生 Android,gitee #IBJBFL 感谢 `@DeanNode` 反馈。 +- :bug: mica-mqtt-client-spring-boot-starter 修复 Spring Boot 3.2 启动时出现警告 gitee #IBITP5 感谢 `@cyber` 反馈。 +- :arrow_up: 升级 mica-net 到 1.0.12,tcp server 如果非 debug 不开启版本信息等打印,方便支持 Android(Android 下没有 ManagementFactory) + +### v2.4.2-M1 - 2025-01-17 +- :sparkles: mica-mqtt client 调整重连重新订阅的逻辑 gitee #IBIE27 感谢 `@cyber` 反馈。 +- :sparkles: mica-mqtt client solon 和 spring boot 插件添加 MQTT5.0 的 sessionExpiryIntervalSecs 配置 gitee #IBIE27 感谢 `@cyber` 反馈。 +- :sparkles: mica-mqtt client DefaultMqttClientSession 移除 final 修饰,方便继承自定义。 +- :sparkles: mica-mqtt client 将 clientId 绑定到 context 上,可以使用 `context.getId()` 获取,gitee #IBHHB1 感谢 `@cv` 反馈。 +- :sparkles: 将 MqttServerCustomizer 和 MqttClientCustomizer 抽到 mica-mqtt-server、mica-mqtt-client 方便组件封装 gitee #IBIJDF 感谢 `@cyber` 反馈 + +### v2.4.1 - 2025-01-04 +- :sparkles: mqtt server 统一 topic 订阅、发布认证日志方便排查问题。 +- :sparkles: mqtt server 添加 PROXY protocol v1 支持,nginx 可开启 tcp proxy_protocol on; 时转发源 ip 信息。 +- :memo: 修复文档 maven 坐标错误。 +- :bug: 修复 spring boot 项目使用全局懒加载 topic无法订阅 gitee #IBFIV8 感谢 `@xixuanhao` 反馈 + +### v2.4.0 - 2024-12-07 +- :sparkles: http api 添加 `stats`、`clients` 列表和 `client详情` 接口。 +- :sparkles: MqttServer 和 MqttServerTemplate 添加 `getClientInfo` `getClients` 系列客户端信息接口。 +- :sparkles: MqttServer 和 MqttServerTemplate 添加 `getSubscriptions` 获取客户端订阅列表接口。 +- :sparkles: MqttServer 和 MqttServerTemplate 添加 `getStat` 统计接口。 +- :truck: 调整 maven groupId `net.dreamlu` 到新的 `org.dromara.mica-mqtt`。 +- :truck: 调整包名 `net.dreamlu.iot.mqtt` 到新的 `org.dromara.mica.mqtt`,其他均保持不变。 +- :truck: 切换到 central sonatype,central sonatype 不支持快照版,mica-mqtt 不再发布快照版。 +- :bug: 修复订阅发送时机问题 gitee #IB72L6 感谢 `@江上烽` 反馈 + +### v2.4.0-M2 - 2024-12-01 +- :sparkles: mica-mqtt-server 暴露获取 clientId 和状态统计接口 +- :bug: 修复订阅发送时机问题 gitee #IB72L6 感谢 `@江上烽` 反馈 + +### v2.4.0-M1 - 2024-11-24 +- :sparkles: 调整 groupId 到 org.dromara.mica-mqtt +- :truck: 切换包名到 org.dromara +- :truck: 切换到 central sonatype, central sonatype 不支持快照版 + +### v2.3.9 - 2024-11-16 +- :sparkles: ssl 支持 **PKCS12** 证书,根据文件后缀自动判断 `.jks、.keystore` 识别为 **JKS证书**,`.p12、.pfx` 识别成 **PKCS12证书**。其他默认成**JKS** +- :sparkles: 优化 **Solon** 版本依赖(仅编译依赖),兼容 `2.8.0` 和 `2.8.0` 以上版本。 + +### v2.3.8 - 2024-09-26 +- :sparkles: 升级到 solon v3, 调整 solon 版本兼容 +- :bug: mica-net 心跳支持了 `keepAliveBackoff`,mica-mqtt 漏改规则(影响范围:mica-mqtt server 开源版,私服版无此问题。) gitee #IAW9FC 感谢 `tan90` 反馈。 + +### v2.3.7 - 2024-09-22 +- :sparkles: 优化 Mqtt server starter 添加 schedule 系列方法。 +- :sparkles: MqttClient schedule 系列方法下层到底层,方法改造。 + +### v2.3.6 - 2024-09-14 +- :sparkles: mica-mqtt server 和 client 优化 stop,支持 stop 后重新配置再启动(注意:需要重新配置,因为老的线程池已经停止)。 +- :sparkles: mica-mqtt server 和 client Spring boot starter 支持 Spring boot devtools 热启动。 +- :sparkles: `FastJsonMessageSerializer` 重构为 `JsonMessageSerializer`。 +- :sparkles: 添加 `module-info.java` 方便模块化。 + +### v2.3.5 - 2024-09-01 +- :sparkles: 新增 `SSLEngineCustomizer`,用于自定义 tls 协议版本和加密套件。 +- :sparkles: !20 修改了 solon 插件的默认配置数值,感谢 `@peigen` pr。 + +### v2.3.4 - 2024-08-10 +- :sparkles: mica-mqtt 合入 `mica-mqtt-client-solon-plugin` 和 `mica-mqtt-server-solon-plugin` 感谢 `@peigenlpy` +- :sparkles: jfinal 插件重命名为 `mica-mqtt-client-jfinal-plugin` 和 `mica-mqtt-server-jfinal-plugin` +- :bug: mica-mqtt-server 修复分组订阅删除,感谢 `@tangjj` 反馈。 + +### v2.3.3 - 2024-07-22 +- :sparkles: mica-mqtt-server 可停止,同步捐助版。 +- :sparkles: mica-mqtt-server 添加 schedule 系列方法,同步捐助版。 +- :sparkles: mica-mqtt 代码优化 TopicUtil 优化 getTopicFilter 方法。 +- :sparkles: mica-mqtt 优化 AckTimerTask 和 retry 重发日志。gitee #IABQ7L 感谢 `@tan90` 反馈。 +- :sparkles: mica-mqtt-client-spring-boot-starter 更加方便自定义 MqttClientTemplate。 +- :sparkles: mica-mqtt-client-spring-boot-starter MqttClientTemplate 暴露更多方法,方便使用。 +- :sparkles: mica-mqtt-example 添加 ssl 测试代码 +- :bug: mica-mqtt-client 修复 ssl 服务端重启问题 gitee #IA9FFW #IAEHOD 感谢 `@geekerstar` `@hangrj` 反馈。 + +### v2.3.1 - 2024-06-25 +- :sparkles: mica-mqtt-server 重构心跳,心跳检测模式默认为:最后接收的数据时间。gitee #I9R0SN #IA69SM 感谢 `@HY` `@tan90` 反馈。 +- :sparkles: mica-mqtt-server 优化端口占用的异常提示,方便排查。 +- :sparkles: mica-mqtt client 使用 mica-net 内置的心跳检测,内置心跳已重构。 +- :sparkles: mica-mqtt-client 重连不管服务端是否存在 session 都发送订阅。gitee #I9VIUV 感谢 `@xiaochonzi` 反馈。 +- :sparkles: 快照版也打 source jar 方便使用。 +- :sparkles: 添加 renovate bot 方便更新依赖和插件版本。 +- :sparkles: 优化 issue.yml 和 github action。 + +### v2.3.0 - 2024-05-26 +- :sparkles: mica-mqtt 优化 MqttQoS 枚举,改为 `MqttQoS.QOS0`,方便使用(不兼容)。 +- :sparkles: mica-mqtt-client 同步私服部分功能,支持 stop 完全停止。 +- :sparkles: mica-mqtt-client 同步私服部分功能,MqttClient 都添加了 `schedule`、`scheduleOnce` 方法,(**耗时任务,请务必自定义线程池**) +- :sparkles: mica-mqtt-server 优化设备离线,简化代码。 +- :sparkles: mica-mqtt-server 用户绑定使用 tio 内置 `Tio.bindUser(context, username)`。 +- :bug: 修复 @MqttClientSubscribe 类型错误时的异常提示。 +- :bug: mica-mqtt-client 修复重连可能失败的问题 gitee #I9RI8E 感谢 `@YYGuo` 反馈。 + +### v2.2.13 - 2024-05-12 +- :sparkles: mica-mqtt-codec MqttVersion 添加版本全名。 +- :sparkles: mica-mqtt-codec MqttConnectReasonCode 添加中文说明。 +- :bug: mica-mqtt-server 保留消息下发时没有订阅也应该先存储 gitee #I9IYX1。 + +### v2.2.12 - 2024-04-16 +- :bug: mica-mqtt-server 遗嘱消息发送判断 + +### v2.2.11 - 2024-04-13 +- :sparkles: mica-mqtt-client-spring-boot-starter 简化 MqttClientTemplate 构造,方便自定义。 +- :sparkles: mica-mqtt-client-spring-boot-starter 优化 spring event mqtt client 连接监听。 +- :sparkles: mica-mqtt-client-spring-boot-starter 优化注解订阅。 +- :bug: mqtt-client 修复 mqtt5 props 和遗嘱同时配置时连接编码问题。 + +### v2.2.10 - 2024-03-23 +- :sparkles: mica-mqtt-client 优化 client publish 时还没有认证的情况。 +- :sparkles: mica-mqtt-client-spring-boot-starter 优化注解订阅,支持 clean session false 重启接收消息。 + +### v2.2.9 - 2024-02-25 +- :sparkles: mica-mqtt-server 拦截器 IMqttMessageInterceptor 添加 onAfterConnected 方法,方便在连接时做黑名单等功能。 +- :sparkles: mica-mqtt-client 添加私服版客户端全局订阅功能和添加使用文档。 +- :boom: mica-mqtt-common 删除弃用的 ThreadUtil。 + +### v2.2.8 - 2024-01-19 +- :sparkles: jfinal-mica-mqtt-client 启动改为同步连接。 +- :bug: mica-mqtt-client 修复 `isConnected` 判断。`2.2.7` 中存在此问题。 +- :arrow_up: 依赖升级 + +### v2.2.7 - 2024-01-03 +- :sparkles: mica-mqtt-server mqttws开启了ssl后,使用mqtt.js去连接,多刷新几次就会超时 gitee #I8LCMY +- :sparkles: mica-mqtt-example 优化 graalvm 配置,感谢 github `@litongjava` 反馈 + +### v2.2.6 - 2023-11-26 +- :sparkles: mica-mqtt-server 添加 `webConfigCustomize` 支持自定义 http 和 ws 配置,可用于 gitee #I8HF7P +- :sparkles: mica-mqtt-client 添加连接测试功能 connectTest gitee #I8J35M 感谢 `@彭蕾` 反馈 +- :sparkles: mica-mqtt-example 更新 graalvm 配置 + +### v2.2.5.1 - 2023-11-01 +- :poop: mica-mqtt-client mqttExecutor 方法参数类型漏改。 + +### v2.2.5 - 2023-10-05 +- :sparkles: mqtt 业务线程池支持自定义设置为 java21虚拟线程。 +- :sparkles: 更新 GitHub action,java17 改为 java21。 +- :sparkles: ThreadUtil 弃用(暂时未删),切换到 mica-net 中的 ThreadUtils。 + +### v2.2.4 - 2023-09-02 +- :sparkles: 合并去年开源之夏的服务端共享订阅和完善(捐助VIP版采用 topic 树存储,跟 topic 数无关,百万 topic 性能依旧)。 +- :sparkles: 优化 topic 检验 +- :bug: 相同 clientId 订阅相同 匹配 topic 应该取最大的qos gitee #I7WWPN + +### v2.2.3 - 2023-07-23 +- :sparkles: mqtt server http api publish 不按 clientId 进行路由(无实际意义),而是按 topic,规则改为同 emqx。 +- :sparkles: mqtt server http api publish 触发 onMessage 消息监听。 +- :arrow_up: 依赖升级 + +### v2.2.2 - 2023-06-17 +- :sparkles: mica-mqtt-client 心跳包日志受 debug 控制 +- :sparkles: mica-mqtt-broker 的集群改为 redis stream 实现。 +- :bug: 修复 starter ssl truststorePass 配置,github #6 感谢 `@zkname` 反馈 + +### v2.2.1 - 2023-05-28 +- :zap: mica-mqtt-client 共享订阅更好的兼容 emqx 高版本,gitee #I786GU +- :arrow_up: 依赖升级 + +### v2.2.0 - 2023-05-14 +- :sparkles: MqttPublishMessage payload 参数均由 `ByteBuffer` 改为 `byte[]`,简化代码,方便使用。 +- :bug: 修复 高并发场景下取消订阅时报 ConcurrentModificationException github #5 感谢 `@yinyuncan` 反馈 + +### v2.1.2 - 2023-04-26 +- :sparkles: mica-mqtt-client 支持 `reconnect(String ip, int port)` 转移到其他服务,订阅保留,连接成功时自动重新订阅。感谢 `@powerxie` 反馈 +- :sparkles: 优化 `TopicUtil#getTopicFilter()` topic 占位符替换。 +- :sparkles: 调整 mica-mqtt-client-spring-boot-starter 启动时机。`MqttClientCustomizer` 支持从数据库中获取配置。感谢 `@powerxie` 反馈 +- :memo: 修复迁移指南**ssl配置**文档错误 +- :bug: 修复包长度计算错误,压测下协议解析异常 gitee #I6YOMD 感谢 `@powerxie` 反馈 + +### v2.1.1 - 2023-04-08 +- :sparkles: mica-mqtt-server http-api 不再强制依赖 `fastjson` 还支持 `Jackson`、`Fastjson2`、`Gson`、`hutool-json` 和自定义, `@皮球` 反馈 gitee #I6O49D。 +- :sparkles: mica-mqtt-codec 删除 `org.dromara.mica.mqtt.codec.ByteBufferUtil`,2.1.0 漏删。 +- :sparkles: mica-mqtt-codec 兼容 qos大于0,messageId == 0,做 qos 降级处理,`@那一刹的容颜` 反馈,详见 gitee #I6PFIH +- :sparkles: mica-mqtt-codec maxClientIdLength 默认改为 64,gitee #I6P2CG +- :sparkles: mica-mqtt-client 优化链接时的遗嘱消息构建,默认为 qos0。`@tan90` 反馈 gitee #I6BRBV +- :bug: mqtt-server 修复 mqtt.js websocket 空包问题,感谢群友反馈。 +- :bug: mqtt-server 修复 websocket mqtt 包长度判断问题。 +- :arrow_up: 依赖升级 + +### v2.1.0 - 2023-03-05 +- :sparkles: 【不兼容】调整接口参数,方便使用 +- :sparkles: 【不兼容】底层重构调整 +- :sparkles: 兼容更多 Spring boot 版本,支持 `2.1.0.RELEASE` 以上版本。 +- :sparkles: ssl 支持双向认证 gitee #I61AHJ 感谢 @DoubleH 反馈 +- :bug: 修复遗嘱消息判断 gitee #I6BRBV 感谢 @tan90 反馈。 +- :bug: 修复错别字 gitee #I6F2PA 感谢 @hpz 反馈 +- :arrow_up: 依赖升级 + +### v2.0.3 - 2022-09-18 +- :sparkles: 完善 ssl 方法,方便使用。 +- :arrow_up: 依赖升级,避免依赖导致的 bug。 + +### v2.0.2 - 2022-09-13 +- :bug: 彻底修复解码异常: `BufferUnderflowException`。 + +### v2.0.1 - 2022-09-12 +- :sparkles: 优化 MqttWebServer 配置。 +- :sparkles: mica-mqtt-example 添加华为云iot连接示例。 +- :sparkles: mica-mqtt-example 改为使用 tinylog。 +- :bug: 修复解码异常: `BufferUnderflowException`。 + +### v2.0.0 - 2022-09-04 +- :sparkles: mica mqtt server 完善方法,方便使用。 +- :sparkles: 切换到自维护的 java8 t-io,注意:升级了 t-io 部分类名变更。 + +### v1.3.9 - 2022-08-28 +- :sparkles: mica-mqtt server 添加消息拦截器,gitee #I5KLST +- :sparkles: mica-mqtt client、server ack 优化和完善,可自定义 ackService。 +- :sparkles: mica-mqtt client stater MqttClientTemplate 完善,统一调整客户端示例。 +- :sparkles: mica-mqtt client 优化客户端心跳和心跳日志优化。 +- :sparkles: mica-mqtt client 订阅代码优化。 +- :sparkles: mica-mqtt codec 代码优化。 +- :sparkles: test 代码优化,更加符合 junit5 规范。 +- :bug: mqtt client Qos2 修复。 + +### v1.3.8 - 2022-08-11 +- :sparkles: mica-mqtt codec 代码优化。 +- :sparkles: mica-mqtt server 使用 Spring event 解耦消息监听。 +- :sparkles: mica-mqtt client stater,@MqttClientSubscribe topic 支持其他变量 ${productKey} 自动替换成 +。 +- :memo: 添加演示地址 +- :bug: 修复 mica-mqtt client 心跳发送问题。gitee #I5LQXV 感谢 `@iTong` 反馈。 + +### v1.3.7 - 2022-07-24 +- :sparkles: 添加 mica-mqtt jfinal client 和 server 插件。 +- :sparkles: mica-mqtt server 代码优化,useQueueDecode 默认为 true。 +- :sparkles: mica-mqtt client 监听回调代码优化。 +- :memo: 添加赞助,让你我走的更远!!! +- :arrow_up: 依赖升级。 + +### v1.3.6 - 2022-06-25 +- :sparkles: mica-mqtt 统一调整最大的消息体和一次读取的字节数。 +- :sparkles: mica-mqtt client 简化 ssl 开启。 +- :sparkles: mica-mqtt server 添加默认的账号密码配置。 +- :arrow_up: 依赖升级 + +### v1.3.4 - 2022-06-06 +- :sparkles: mica-mqtt starter 使用 Spring event 解耦 mqtt client 断连事件。 +- :sparkles: mica-mqtt server `IMqttConnectStatusListener#offline` 方法添加 `reason` 断开原因字段。 +- :sparkles: 添加赞助计划。**捐助共勉,让你我走的更远!!!** +- :bug: 修复 http api 响应问题。 + +### v1.3.3 - 2022-05-28 +- :sparkles: mica-mqtt 优化线程池。 +- :sparkles: mica-mqtt 添加 Compression 压缩接口。 +- :sparkles: mica-mqtt 添加 kafka TimingWheel 重构 ack。 +- :sparkles: mica-mqtt server 添加 `MqttClusterMessageListener` 方便集群消息处理。 +- :sparkles: mica-mqtt client 优化客户端取消订阅逻辑,gitee #I5779A 感谢 `@杨钊` 同学反馈。 +- :arrow_up: 升级 fastjson 到 1.2.83。 + +### v1.3.2 - 2022-05-09 +- :sparkles: mica-mqtt topic 匹配完善。 +- :sparkles: mica-mqtt 订阅、发布时添加 topicFilter、topicName 校验。 + +### v1.3.1 - 2022-05-08 +- :sparkles: mica-mqtt-broker 默认开启 http 和 basic auth。 +- :sparkles: mica-mqtt server 添加服务端共享订阅接口,方便开源之夏学生参与。 +- :sparkles: mica-mqtt server 添加 IMqttSessionListener。 +- :sparkles: mica-mqtt server publish 保留消息存储。 +- :sparkles: mica-mqtt server 统一 http 响应模型、优化 http 请求判断。 +- :sparkles: mica-mqtt server 优化 MqttHttpRoutes,添加获取所有路由的方法。 +- :sparkles: mica-mqtt server 完善 Result 和 http api。 +- :sparkles: mica-mqtt server http api 添加 endpoints 列表接口。 +- :sparkles: mica-mqtt client 添加同步连接 connectSync 方法。 +- :sparkles: mica-mqtt client 优化 bean 依赖,减少循环依赖可能性。 +- :bug: 重构 mqtt topic 匹配规则,提升性能减少内存占用,修复 gitee #I56BTC +- :arrow_up: spring boot、mica 版本升级 + +### v1.3.0 - 2022-04-17 +- :sparkles: mica-mqtt mqtt-server 简化,默认多设备可以直接互相订阅和处理消息。 +- :sparkles: mica-mqtt server、client 添加 `tioConfigCustomize` 方法,方便更大程度的自定义 TioConfig。 +- :sparkles: 拆分 mica-mqtt-client-spring-boot-starter 和 mica-mqtt-server-spring-boot-starter gitee #I4OTC5 +- :sparkles: mica-mqtt-client-spring-boot-example 添加重连动态更新 clientId、username、password 示例。 +- :sparkles: mica-mqtt server 添加根据踢出指定 clientId 的 http api 接口。 +- :sparkles: mica-mqtt server IMqttConnectStatusListener api 调整,添加用户名字段。 +- :sparkles: mica-mqtt server IMqttMessageListener 不再强制要求实现。 +- :sparkles: 使用 netty IntObjectHashMap 优化默认 session 存储。 +- :sparkles: 添加 github action,用于自动构建开发阶段的 SNAPSHOT 版本。 +- :sparkles: 示例项目拆分到 example 目录,mica-mqtt client、server starter 拆分到 starter 目录。 +- :arrow_up: 依赖升级. + +### v1.2.10 - 2022-03-20 +- :sparkles: mica-mqtt server 添加 MQTT 客户端 keepalive 系数 `keepalive-backoff`。 +- :sparkles: mica-mqtt client、server 调整发布的日志级别为 debug。 +- :sparkles: mica-mqtt client 优化 javadoc。 +- :sparkles: mica-mqtt client 重连时,支持重新设置新的鉴权密码。 + +### v1.2.9 - 2022-03-06 +- :sparkles: mqttServer#publishAll() 日志级别调整 gitee #I4W4IS +- :sparkles: @MqttClientSubscribe 支持 springboot 配置 gitee #I4UOR3 +- :sparkles: mica-mqtt client 代码优化 +- :sparkles: mica-mqtt-spring-boot-example 拆分 + +### v1.2.8 - 2022-02-20 +- :sparkles: mica-mqtt server 优化连接 connect 日志。 +- :sparkles: mica-mqtt server 代码优化。 +- :sparkles: mica-mqtt server 添加 statEnable 配置,默认关闭,开启 Prometheus 监控,需要设置为 true。 +- :sparkles: mica-mqtt client 添加 statEnable 配置,默认关闭。 +- :sparkles: mica-mqtt client 优化默认线程池。 + +### v1.2.7 - 2022-02-13 +- :sparkles: mica-mqtt-spring-boot-starter 完善。 +- :sparkles: mica-mqtt client 考虑一开始就没有连接上服务端的情况。 +- :sparkles: mica-mqtt client 添加 isConnected 方法 +- :sparkles: mica-mqtt client、server connectListener 改为异步 +- :sparkles: mica-mqtt server ChannelContext 添加用户名,使用 (String) context.get(MqttConst.USER_NAME_KEY) 获取。 +- :sparkles: websocket ssl 配置 +- :sparkles: 尝试新版 graalvm +- :bug: 修复多个 mica mqtt client 消息id生成器隔离。 + +### v1.2.6 - 2022-01-19 +- :sparkles: mica-mqtt-client 支持 `$share`、`$queue` 共享订阅 + +### v1.2.5 - 2022-01-16 +- :sparkles: mica mqtt server 调整发布权限规则。 +- :sparkles: mica mqtt server 自定义接口的异常处理。 +- :sparkles: mica mqtt server 放开 tio 队列配置。 +- :sparkles: mica mqtt client publish 添加一批 byte[] payload 参数方法。 +- :sparkles: mica-mqtt-model DefaultMessageSerializer 重构,**不兼容**。 +- :memo: 添加日志,避免遗忘。 +- :bug: http websocket 都不开启并排除 tio-websocket-server 依赖时 gitee #I4Q3CP + +### v1.2.4 - 2022-01-09 +- :fire: mica-mqtt-core 排除一些不需要的依赖。 +- :fire: mica-mqtt-core http websocket 都不开启时,可以排除 tio-websocket-server 依赖。 +- :sparkles: mica-mqtt-core MqttTopicUtil 改名为 TopicUtil。 +- :sparkles: mica-mqtt-spring-boot-starter `@MqttClientSubscribe` 支持 IMqttClientMessageListener bean。 +- :sparkles: mica-mqtt-spring-boot-starter `@MqttClientSubscribe` 支持自定义 MqttClientTemplate Bean。 +- :sparkles: mica-mqtt-spring-boot-starter 完善。 +- :sparkles: mica-mqtt-codec 缩短 mqtt 版本 key。 +- :bug: mica-mqtt-codec 修复 will message。 + +### v1.2.3 - 2022-01-03 +- :sparkles: mica-mqtt-spring-boot-starter `@MqttClientSubscribe` value 改为数组,支持同时订阅多 topic。 +- :sparkles: mica-mqtt-core 缓存 TopicFilter Pattern。 +- :sparkles: mica-mqtt-core 优化客户端和服务端订阅逻辑 `IMqttServerSubscribeValidator` 接口调整。 +- :sparkles: mica-mqtt client 添加批量订阅。 +- :sparkles: mica-mqtt client 添加批量取消订阅。 +- :sparkles: mica-mqtt client 添加客户端是否断开连接。 +- :sparkles: mica-mqtt client 客户端断开重新订阅时支持配置批次大小。 +- :bookmark: mica-mqtt client 订阅 `IMqttClientMessageListener` 添加 `onSubscribed` 默认方法。 +- :arrow_up: mica-mqtt-example 升级 log4j2 到 2.17.1 + +### v1.2.2 - 2021-12-26 +- :sparkles: mica-mqtt server 添加发布权限接口,无权限直接断开连接,避免高级别 qos 重试浪费资源。 +- :sparkles: mica-mqtt-broker 优化节点信息存储 +- :sparkles: mica-mqtt client 重复订阅优化。感谢 `@一片小雨滴` +- :sparkles: mica-mqtt client 抽象 IMqttClientSession 接口。 +- :bug: 修复重构 AbstractMqttMessageDispatcher 保持跟 mica-mqtt-broker 逻辑一致 gitee #I4MA6A 感谢 `@胡萝博` +- :arrow_up: mica-mqtt-example 升级 log4j2 到 2.17.0 + +### v1.2.1 - 2021-12-11 +- :sparkles: mica-mqtt 优化 topic 匹配。 +- :sparkles: mica-mqtt client disconnect 不再自动重连 gitee #I4L0WK 感谢 `@willianfu`。 +- :sparkles: mica-mqtt client 添加 retryCount 配置 gitee #I4L0WK 感谢 `@willianfu`。 +- :sparkles: mica-mqtt-model message 添加 json 序列化。 +- :sparkles: mica-mqtt-broker 重新梳理逻辑。 +- :bug: mica-mqtt-spring-boot-starter 在 boot 2.6.x 下 bean 循环依赖 gitee #I4LUZP 感谢 `@hongfeng11`。 +- :bug: mica-mqtt server 同一个 clientId 踢出时清除老的 session。 +- :bug: mica-mqtt server 集群下一个 clientId 只允许连接到一台服务器。 +- :bug: mica-mqtt client 修复 IMqttClientConnectListener onDisconnect 空指针。 +- :memo: mica-mqtt-model 添加 README.md + +### v1.2.0 - 2021-11-28 +- :sparkles: mqtt-mqtt-core client IMqttClientConnectListener 添加 onDisconnect 方法。gitee #I4JT1D 感谢 `@willianfu` 同学反馈。 +- :sparkles: mica-mqtt-core server IMqttMessageListener 接口调整,不兼容老版本。 +- :sparkles: mica-mqtt-broker 调整上下行消息通道。 +- :sparkles: mica-mqtt-broker 添加节点管理。 +- :sparkles: mica-mqtt-broker 调整默认的 Message 序列化方式,不兼容老版本。 +- :sparkles: mica-mqtt-broker 优化设备上下线,处理节点停机的情况。 +- :sparkles: 抽取 mica-mqtt-model 模块,方便后期支持消息桥接,Message 添加默认的消息序列化。 gitee #I4ECEO +- :sparkles: mica-mqtt-model 完善 Message 消息模型,方便集群。 +- :bug: mica-mqtt-core MqttClient 修复 ssl 没有设置,感谢 `@hjkJOJO` 同学反馈。 +- :bug: 修复 websocket mqtt.js 需要拆包 gitee #I4JYJX 感谢 `@Symous` 同学反馈。 +- :memo: 完善 mica-mqtt-broker README.md,添加二开说明。 +- :memo: 统一 mica-mqtt server ip 文档。 +- :memo: 更新 README.md +- :arrow_up: 升级 tio 到 3.7.5.v20211028-RELEASE AioDecodeException 改为 TioDecodeException, + +### v1.1.4 - 2021-10-16 +- :sparkles: 添加 uniqueId 概念,用来处理 clientId 不唯一的场景。详见:gitee #I4DXQU +- :sparkles: 微调 `IMqttServerAuthHandler` 认证,添加 uniqueId 参数。 + +### v1.1.3 - 2021-10-13 +- :sparkles: 状态事件接口 `IMqttConnectStatusListener` 添加 ChannelContext 参数。 +- :sparkles: 从认证中拆分 `IMqttServerSubscribeValidator` 订阅校验接口,添加 ChannelContext、clientId 参数。 +- :sparkles: 认证 `IMqttServerAuthHandler` 调整包、添加 ChannelContext 参数。 +- :sparkles: 完善文档和示例,添加默认端口号说明。 +- :arrow_up: 依赖升级 + +### v1.1.2 - 2021-09-12 +- :sparkles: 添加 mica-mqtt-broker 模块,基于 redis pub/sub 实现 mqtt 集群。 +- :sparkles: mica-mqtt-broker 基于 redis 实现客户端状态存储。 +- :sparkles: mica-mqtt-broker 基于 redis 实现遗嘱、保留消息存储。 +- :sparkles: mqtt-server http api 调整订阅和取消订阅,方便集群处理。 +- :sparkles: mica-mqtt-spring-boot-example 添加 mqtt 和 http api 认证示例。 +- :sparkles: 添加 mqtt 5 所有 ReasonCode。 +- :sparkles: 优化解码 PacketNeededLength 计算。 +- :bug: 修复遗嘱消息,添加消息类型。 +- :bug: 修复 mqtt-server 保留消息匹配规则。 + +### v1.1.1 - 2021-09-05 +- :sparkles: mqtt-server 优化连接关闭日志。 +- :sparkles: mqtt-server 优化订阅,相同 topicFilter 订阅对 qos 判断。 +- :sparkles: mqtt-server 监听器添加 try catch,避免因业务问题导致连接断开。 +- :sparkles: mqtt-server 优化 topicFilters 校验。 +- :sparkles: mqtt-client 优化订阅 reasonCodes 判断。 +- :sparkles: mqtt-client 监听器添加 try catch,避免因业务问题导致连接断开。 +- :sparkles: mqtt-client 添加 session 有效期。 +- :sparkles: 代码优化,减少 codacy 上的问题。 +- :bug: mqtt-server 修复心跳时间问题。 +- :bug: 修复 mqtt-server 多个订阅同时匹配时消息重复的问题。 +- :bug: mqtt-client 优化连接处理的逻辑,mqtt 连接之后再订阅。 +- :bug: 修复 MqttProperties 潜在的一个空指针。 + +### v1.1.0 - 2021-08-29 +- :sparkles: 重构,内置 http,http 和 websocket 公用端口。 +- :sparkles: 添加 mica-core 中的 HexUtil。 +- :sparkles: 添加 PayloadEncode 工具。 +- :sparkles: ServerTioConfig#share 方法添加 groupStat。 +- :sparkles: 考虑使用 udp 多播做集群。 +- :sparkles: MqttServer、MqttServerTemplate 添加 close、getChannelContext 等方法。 +- :sparkles: 重构 MqttServerConfiguration 简化代码。 +- :sparkles: 配置项 `mqtt.server.websocket-port` 改为 `mqtt.server.web-port`。 +- :memo: 添加 JetBrains 连接。 +- :bug: 修复默认的消息转发器逻辑。 +- :bug: 修复 websocket 下线无法触发offline gitee #I47K13 感谢 `@willianfu` 同学反馈。 + +### v1.0.6 - 2021-08-21 +- :sparkles: 添加订阅 topicFilter 校验。 +- :sparkles: 优化压测工具,更新压测说明,添加 tcp 连接数更改文档地址。 +- :sparkles: mica-mqtt-example 添加多设备交互示例。 +- :sparkles: 优化 mica-mqtt-spring-boot-example。 +- :sparkles: 优化 deploy.sh 脚本。 +- :bug: 优化解码异常处理。 +- :bug: 修复心跳超时处理。 +- :arrow_up: 升级 spring boot 到 2.5.4 + +### v1.0.5 - 2021-08-15 +- :bug: 修复编译导致的 java8 运行期间的部分问题,NoSuchMethodError: java.nio.ByteBuffer.xxx + +### v1.0.3 - 2021-08-15 +- :sparkles: mica-mqtt server 添加 websocket mqtt 子协议支持(支持 mqtt.js)。 +- :sparkles: mica-mqtt server ip,默认为空,可不设置。 +- :sparkles: mica-mqtt client去除 CountDownLatch 避免启动时未连接上服务端卡住。 +- :sparkles: mica-mqtt client 添加最大包体长度字段,避免超过 8092 长度的包体导致解析异常。 +- :sparkles: mica-mqtt client 添加连接监听 IMqttClientConnectListener。 +- :sparkles: mica-mqtt 3.1 协议会校验 clientId 长度,添加配置项 maxClientIdLength。 +- :sparkles: mica-mqtt 优化 mqtt 解码异常处理。 +- :sparkles: mica-mqtt 日志优化,方便查询。 +- :sparkles: mica-mqtt 代码优化,部分 Tio.close 改为 Tio.remove。 +- :sparkles: mica-mqtt-spring-boot-example 添加 Dockerfile,支持 `spring-boot:build-image`。 +- :sparkles: 完善 mica-mqtt-spring-boot-starter,添加遗嘱消息配置。 +- :arrow_up: 升级 t-io 到 3.7.4。 + +### v1.0.3-RC - 2021-08-12 +- :sparkles: 添加 websocket mqtt 子协议支持(支持 mqtt.js)。 +- :sparkles: mqtt 客户端去除 CountDownLatch 避免启动时未连接上服务端卡住。 +- :sparkles: mica-mqtt 服务端 ip,默认为空,可不设置。 +- :sparkles: 完善 mica-mqtt-spring-boot-starter,添加遗嘱消息配置。 +- :sparkles: mqtt 3.1 协议会校验 clientId 长度,添加设置。 +- :sparkles: mqtt 日志优化,方便查询。 +- :sparkles: 代码优化,部分 Tio.close 改为 Tio.remove。 +- :arrow_up: 升级 t-io 到 3.7.4。 + +### v1.0.2 - 2021-08-08 +- :memo: 文档添加集群处理步骤说明,添加遗嘱消息、保留消息的使用场景。 +- :sparkles: 去除演示中的 qos2 参数,性能损耗大避免误用。 +- :sparkles: 遗嘱、保留消息内部消息转发抽象。 +- :sparkles: mqtt server 连接时先判断 clientId 是否存在连接关系,有则先关闭已有连接。 +- :sparkles: 添加 mica-mqtt-spring-boot-example 。感谢 wsq( @冷月宫主 )pr。 +- :sparkles: mica-mqtt-spring-boot-starter 支持客户端接入和服务端优化。感谢 wsq( @冷月宫主 )pr。 +- :sparkles: mica-mqtt-spring-boot-starter 服务端支持指标收集。可对接 `Prometheus + Grafana` 监控。 +- :sparkles: mqtt server 接受连接时,先判断该 clientId 是否存在其它连接,有则解绑并关闭其他连接。 +- :arrow_up: 升级 mica-auto 到 2.1.3 修复 ide 多模块增量编译问题。 + +### v1.0.2-RC - 2021-08-04 +- :sparkles: 添加 mica-mqtt-spring-boot-example 。感谢 wsq( @冷月宫主 )pr。 +- :sparkles: mica-mqtt-spring-boot-starter 支持客户端接入和服务端优化。感谢 wsq( @冷月宫主 )pr。 +- :sparkles: mica-mqtt-spring-boot-starter 服务端支持指标收集。可对接 `Prometheus + Grafana` 监控。 +- :sparkles: mqtt server 接受连接时,先判断该 clientId 是否存在其它连接,有则解绑并关闭其他连接。 + +### v1.0.1 - 2021-08-02 +- :sparkles: 订阅管理集成到 session 管理中。 +- :sparkles: MqttProperties.MqttPropertyType 添加注释,考虑 mqtt V5.0 新特性处理。 +- :sparkles: 添加 Spring boot starter 方便接入,兼容低版本 Spring boot。 +- :sparkles: 调研 t-io websocket 子协议。 +- :bug: 修复 java8 运行期间的部分问题,NoSuchMethodError: java.nio.ByteBuffer.xxx + +### v1.0.1-RC - 2021-07-31 +- :sparkles: 添加 Spring boot starter 方便接入。 +- :sparkles: 调研 t-io websocket 子协议。 + +### v1.0.0 - 2021-07-29 +- :sparkles: 基于低延迟高性能的 t-io AIO 框架。 +- :sparkles: 支持 MQTT v3.1、v3.1.1 以及 v5.0 协议。 +- :sparkles: 支持 MQTT client 客户端。 +- :sparkles: 支持 MQTT server 服务端。 +- :sparkles: 支持 MQTT 遗嘱消息。 +- :sparkles: 支持 MQTT 保留消息。 +- :sparkles: 支持自定义消息(mq)处理转发实现集群。 +- :sparkles: 支持 GraalVM 编译成本机可执行程序。 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a91ce55 --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "{}" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net). + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..08305e5 --- /dev/null +++ b/README.en.md @@ -0,0 +1,129 @@ +# 🌐 Dromara mica mqtt +[![Java CI](https://github.com/dromara/mica-mqtt/workflows/Java%20CI/badge.svg)](https://github.com/dromara/mica-mqtt/actions) +![JAVA 8](https://img.shields.io/badge/JDK-1.8+-brightgreen.svg) +[![Mica Maven release](https://img.shields.io/maven-central/v/org.dromara.mica-mqtt/mica-mqtt-codec?style=flat-square)](https://central.sonatype.com/artifact/org.dromara.mica-mqtt/mica-mqtt-codec/versions) +![Mica Maven SNAPSHOT](https://img.shields.io/maven-metadata/v?metadataUrl=https://central.sonatype.com/repository/maven-snapshots/org/dromara/mica-mqtt/mica-mqtt-codec/maven-metadata.xml) +[![GitHub](https://img.shields.io/github/license/dromara/mica-mqtt.svg?style=flat-square)](https://github.com/dromara/mica-mqtt/blob/master/LICENSE) + +[![star](https://gitcode.com/dromara/mica-mqtt/star/badge.svg)](https://gitcode.com/dromara/mica-mqtt) +[![star](https://gitee.com/dromara/mica-mqtt/badge/star.svg?theme=dark)](https://gitee.com/dromara/mica-mqtt/stargazers) +[![GitHub Repo stars](https://img.shields.io/github/stars/dromara/mica-mqtt?label=Github%20Stars)](https://github.com/dromara/mica-mqtt) + +--- + +📖English | [📖简体中文](README.md) + +Dromara `mica-mqtt` is a **low-latency, high-performance** MQTT IoT component designed for seamless integration and scalability. For detailed usage guides, refer to the **mica-mqtt-example** module. + +## 🍱 Use Cases + +- Internet of Things (cloud-based MQTT broker) +- Internet of Things (edge messaging communication) +- Group IM +- Message push +- Easy-to-use MQTT client + +## 🚀 Key Advantages +- **Simplicity & Flexibility**: Intuitive design for easy integration while retaining extensibility for advanced use cases. +- **Manual Control**: Explicit API design to facilitate secondary development and customization. +- **Future-ready**: Built with scalability in mind, supporting evolving IoT requirements. + +## ✨ Features +- [x] Supports MQTT v3.1, v3.1.1, and v5.0 protocols. +- [x] Supports WebSocket MQTT sub-protocol (fully compatible with mqtt.js). +- [x] Supports HTTP REST API - see [HTTP API Documentation](docs/http-api.md) for details. +- [x] Support for MQTT client, support Android native. +- [x] Support for MQTT server, support Android native. +- [x] Support for MQTT Will messages. +- [x] Support for MQTT Retained messages. +- [x] Support for custom message (MQ) processing and forwarding to achieve clustering. +- [x] MQTT client **Alibaba Cloud MQTT**、**HuaWei MQTT** connection demo. +- [x] Support for GraalVM compilation into native executable programs. +- [x] Support for rapid access to Spring Boot、Solon and JFinal projects. +- [x] Spring boot and Solon client plugins support session retention. +- [x] Support for integration with Prometheus + Grafana for monitoring. +- [x] Cluster implementation based on Redis pub/sub, see [mica-mqtt-broker module](mica-mqtt-broker) for details. + +## 🌱 To-do + +- [ ] Optimize the handling of MQTT server sessions and simplify the use of MQTT v5.0. +- [ ] Implement rule engine based on easy-rule + druid sql parsing. + +## 🚨 Default Ports + +| Port | Protocol | Description | +|-------|---------------|-------------------------| +| 1883 | tcp | mqtt tcp port | +| 8883 | tcp ssl | mqtt tcp ssl port | +| 8083 | websocket | websocket mqtt port | +| 8084 | websocket ssl | websocket ssl mqtt port | +| 18083 | http | http、MCP api port | + +**Demo Address**: mqtt.dreamlu.net, same ports,username: mica password: mica + +## 📦️ Dependencies + +### Spring Boot Project +**Client:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-client-spring-boot-starter + ${mica-mqtt.version} + +``` + +**Configuration Details**: [mica-mqtt-client-spring-boot-starter Documentation](starter/mica-mqtt-client-spring-boot-starter/README.md) + +**Server:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-server-spring-boot-starter + ${mica-mqtt.version} + +``` + +**Configuration Details**: [mica-mqtt-server-spring-boot-starter Documentation](starter/mica-mqtt-server-spring-boot-starter/README.md) + +### Non-Spring Boot Project + +**Client:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-client + ${mica-mqtt.version} + +``` + +**Configuration Details**: [mica-mqtt-client Documentation](mica-mqtt-client/README.md) + +**Server:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-server + ${mica-mqtt.version} + +``` + +**Configuration Details**: [mica-mqtt-server Documentation](mica-mqtt-server/README.md) + +## 📝 Documentation +- [Introduction to MQTT, mqttx, and mica-mqtt **Video**](https://www.bilibili.com/video/BV1wv4y1F7Av/) +- [Getting Started with mica-mqtt](example/README.md) +- [mica-mqtt HTTP API Documentation](docs/http-api.md) +- [Frequently Asked Questions about mica-mqtt Usage](https://mica-mqtt.dreamlu.net/faq/faq.html) +- [mica-mqtt Release Versions](CHANGELOG.md) + +## 🍻 Open Source Recommendations +- `Avue`: A Vue-based configurable front-end framework: [https://gitcode.com/superwei/avue](https://gitcode.com/superwei/avue) +- `Pig`: Microservice framework featured on CCTV (architectural essential): [https://gitcode.com/pig-mesh/pig](https://gitcode.com/pig-mesh/pig) +- `SpringBlade`: Enterprise-level solution (essential for enterprise development): [https://gitcode.com/bladex/SpringBlade](https://gitcode.com/bladex/SpringBlade) + +## 📱 WeChat + +![DreamLuTech](docs/img/dreamlu-weixin.jpg) + +**JAVA Architecture Diary**, daily recommended exciting content! \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3cd3e84 --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ +# 🌐 Dromara mica mqtt 组件 +[![Java CI](https://github.com/dromara/mica-mqtt/workflows/Java%20CI/badge.svg)](https://github.com/dromara/mica-mqtt/actions) +![JAVA 8](https://img.shields.io/badge/JDK-1.8+-brightgreen.svg) +[![Mica Maven release](https://img.shields.io/maven-central/v/org.dromara.mica-mqtt/mica-mqtt-codec?style=flat-square)](https://central.sonatype.com/artifact/org.dromara.mica-mqtt/mica-mqtt-codec/versions) +![Mica Maven SNAPSHOT](https://img.shields.io/maven-metadata/v?metadataUrl=https://central.sonatype.com/repository/maven-snapshots/org/dromara/mica-mqtt/mica-mqtt-codec/maven-metadata.xml) +[![GitHub](https://img.shields.io/github/license/dromara/mica-mqtt.svg?style=flat-square)](https://github.com/dromara/mica-mqtt/blob/master/LICENSE) + +[![star](https://gitcode.com/dromara/mica-mqtt/star/badge.svg)](https://gitcode.com/dromara/mica-mqtt) +[![star](https://gitee.com/dromara/mica-mqtt/badge/star.svg?theme=dark)](https://gitee.com/dromara/mica-mqtt/stargazers) +[![GitHub Repo stars](https://img.shields.io/github/stars/dromara/mica-mqtt?label=Github%20Stars)](https://github.com/dromara/mica-mqtt) + +--- + +📖简体中文 | [📖English](README.en.md) + +Dromara `mica-mqtt` **低延迟**、**高性能**的 `mqtt` 物联网组件。更多使用方式详见: **mica-mqtt-example** 模块。 + +✨✨✨**最佳实践**✨✨✨ [**BladeX 物联网平台(「mica-mqtt加强版」+「EMQX+Kafka插件」双架构)**](https://iot.bladex.cn?from=mica-mqtt) + +## 🍱 使用场景 + +- 物联网(云端 mqtt broker) +- 物联网(边缘端消息通信) +- 群组类 IM +- 消息推送 +- 简单易用的 mqtt 客户端 + +## 🚀 优势 +- ✓ 轻如蝉翼 - 核心依赖仅 500KB +- ✓ 新手友好 - 5 分钟极速上手 +- ✓ 自由操控 - 手动档设计,扩展随心 +- ✓ 潜力无限 - 小而强大,未来可期 + +## ✨ 功能 +- [x] 支持 MQTT v3.1、v3.1.1 以及 v5.0 协议。 +- [x] 支持 websocket mqtt 子协议(支持 mqtt.js)。 +- [x] 支持 http rest api,[http api 文档详见](docs/http-api.md)。 +- [x] 支持 MQTT client 客户端,支持 **Android** 最低要求 API 26(Android 8.0)。 +- [x] 支持 MQTT server 服务端,支持 **Android** 最低要求 API 26(Android 8.0)。 +- [x] 支持 MQTT client、server 共享订阅支持。 +- [x] 支持 MQTT 遗嘱消息。 +- [x] 支持 MQTT 保留消息。 +- [x] 支持自定义消息(mq)处理转发实现集群。 +- [x] MQTT 客户端 **阿里云 mqtt**、**华为云 mqtt** 连接 demo 示例。 +- [x] 支持 GraalVM 编译成本机可执行程序。 +- [x] 支持 Spring boot、Solon 和 JFinal 项目快速接入。 +- [x] Spring boot、Solon client 插件支持保留 session。 +- [x] 支持对接 Prometheus + Grafana 实现监控。 +- [x] 基于 redis stream 实现集群,详见 [mica-mqtt-broker 模块](https://gitee.com/dromara/mica-mqtt/tree/2.4.x/mica-mqtt-broker)。 +- [x] [mica mqtt 控制台](https://gitee.com/dreamlu/mica-mqtt-dashboard) + +## 🌱 待办 + +- [ ] 优化处理 mqtt 服务端 session,以及简化 mqtt v5.0 使用。 +- [ ] 基于 easy-rule + druid sql 解析,实现规则引擎。 + +## 🚨 默认端口 + +| 端口号 | 协议 | 说明 | +|-------|---------------|--------------------------| +| 1883 | tcp | mqtt tcp 端口 | +| 8883 | tcp ssl | mqtt tcp ssl 端口 | +| 8083 | websocket | websocket mqtt 子协议端口 | +| 8084 | websocket ssl | websocket ssl mqtt 子协议端口 | +| 18083 | http | http、大模型 MCP 接口端口 | + +**演示地址**:mqtt.dreamlu.net 端口同上,账号:mica 密码:mica + +## 📦️ 依赖 + +### Spring boot 项目 +**客户端:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-client-spring-boot-starter + ${mica-mqtt.version} + +``` + +**配置详见**:[mica-mqtt-client-spring-boot-starter 使用文档](starter/mica-mqtt-client-spring-boot-starter/README.md) + +**服务端:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-server-spring-boot-starter + ${mica-mqtt.version} + +``` + +**配置详见**:[mica-mqtt-server-spring-boot-starter 使用文档](starter/mica-mqtt-server-spring-boot-starter/README.md) + +### Solon 项目 +**客户端:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-client-solon-plugin + ${mica-mqtt.version} + +``` + +**配置详见**:[mica-mqtt-client-solon-plugin 使用文档](starter/mica-mqtt-client-solon-plugin/README.md) + +**服务端:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-server-solon-plugin + ${mica-mqtt.version} + +``` + +**配置详见**:[mica-mqtt-server-solon-plugin 使用文档](starter/mica-mqtt-server-solon-plugin/README.md) + +### JFinal 项目 +**客户端:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-client-jfinal-plugin + ${mica-mqtt.version} + +``` + +**配置详见**:[mica-mqtt-client-jfinal-plugin 使用文档](starter/mica-mqtt-client-jfinal-plugin/README.md) + +**服务端:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-server-jfinal-plugin + ${mica-mqtt.version} + +``` + +**配置详见**:[mica-mqtt-server-jfinal-plugin 使用文档](starter/mica-mqtt-server-jfinal-plugin/README.md) + +### 其他项目 + +**客户端:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-client + ${mica-mqtt.version} + +``` + +**配置详见**:[mica-mqtt-client 使用文档](mica-mqtt-client/README.md) + +**服务端:** +```xml + + org.dromara.mica-mqtt + mica-mqtt-server + ${mica-mqtt.version} + +``` + +**配置详见**:[mica-mqtt-server 使用文档](mica-mqtt-server/README.md) + +## 📝 文档 +- [mqtt科普、mqttx、mica-mqtt的使用**视频**](https://www.bilibili.com/video/BV1wv4y1F7Av/) +- [mica-mqtt 快速开始](https://mica-mqtt.dreamlu.net/guide/) +- [mica-mqtt 使用常见问题汇总](https://mica-mqtt.dreamlu.net/faq/faq.html) +- [mica-mqtt 发行版本](https://mica-mqtt.dreamlu.net/version/changelog.html) +- [mica-mqtt 老版本迁移指南](https://mica-mqtt.dreamlu.net/version/update.html) + +## 🍻 开源推荐 +- `Avue` 基于 vue 可配置化的前端框架:[https://gitcode.com/superwei/avue](https://gitcode.com/superwei/avue) +- `pig` 上央视的微服务框架(架构必备):[https://gitcode.com/pig-mesh/pig](https://gitcode.com/pig-mesh/pig) +- `SpringBlade` 企业级解决方案(企业开发必备):[https://gitcode.com/bladex/SpringBlade](https://gitcode.com/bladex/SpringBlade) + +## 📱 微信 + +![如梦技术](docs/img/dreamlu-weixin.jpg) + +**JAVA架构日记**,精彩内容每日推荐! \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e3fc2e8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy(安全策略) + +## Supported Versions(支持的版本) + +| Version | Supported | +|---------|--------------------| +| 2.4.x | :white_check_mark: | +| < 2.4.x | :x: | + +## Reporting a Vulnerability(报告漏洞) + +如果你发现有安全问题或漏洞,请发送邮件到 `596392912@qq.com`。 + +To report any found security issues or vulnerabilities, please send a mail to `596392912@qq.com`. diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..cc3d08b --- /dev/null +++ b/deploy.sh @@ -0,0 +1,43 @@ + +#!/bin/sh + +## 0. java +if command -v vfox >/dev/null 2>&1; then + vfox use java@8.0.342+7 +else + echo "Warning: vfox command not found, skipping Java version switch" +fi + +## 1. java version +java -version +printf "\n" + +## 2. mvn version +mvn -version +printf "\n" + +## 3. 环境 +if [ -z $1 ]; then + profile="release" +else + profile="$1" +fi +printf "profile [%s] \n" "$profile" + +## 4. modules +modules="mica-mqtt-codec,mica-mqtt-common," +modules="$modules mica-mqtt-client,mica-mqtt-server," +modules="$modules starter/mica-mqtt-client-spring-boot-starter," +modules="$modules starter/mica-mqtt-server-spring-boot-starter," +modules="$modules starter/mica-mqtt-client-solon-plugin," +modules="$modules starter/mica-mqtt-server-solon-plugin," +modules="$modules starter/mica-mqtt-client-jfinal-plugin," +modules="$modules starter/mica-mqtt-server-jfinal-plugin" +printf "modules [%s] \n" "$modules" + +## 5. deploy +if [ "$profile" = "snapshot" ]; then + mvn clean deploy -U -P!develop,snapshot -pl "$modules" +else + mvn clean deploy -Prelease -pl "$modules" +fi diff --git a/docs/graalvm.md b/docs/graalvm.md new file mode 100644 index 0000000..612dcdf --- /dev/null +++ b/docs/graalvm.md @@ -0,0 +1,2 @@ +# mica-mqtt 之 GraalVM native-image 编译成本机可执行程序 + diff --git a/docs/http-api.md b/docs/http-api.md new file mode 100644 index 0000000..30e6aa3 --- /dev/null +++ b/docs/http-api.md @@ -0,0 +1,426 @@ +# Http Api 接口 + +注意:mica-mqtt 2.5.x 开始,http api 端口默认 18083(老版本~~8083~~)。 + +### HTTP 状态码 (status codes) + +接口在调用成功时总是返回 200 OK,响应内容则以 JSON 格式返回。 + +可能的状态码如下: + +| Status Code | Description | +| ----------- | -------------------------------------------------------- | +| 200 | 成功,返回的 JSON 数据将提供更多信息 | +| 400 | 客户端请求无效,例如请求体或参数错误 | +| 401 | 客户端未通过服务端认证,使用无效的身份验证凭据可能会发生 | +| 404 | 找不到请求的路径或者请求的对象不存在 | +| 405 | 请求方法错误 | +| 500 | 服务端处理请求时发生内部错误 | + +### 返回码 (result codes) + +接口的响应消息体为 JSON 格式,其中总是包含返回码 `code`。 + +可能的返回码如下: + +| Return Code | Description | +| ----------- | -------------------------- | +| 1 | 成功 | +| 101 | 关键请求参数缺失 | +| 102 | 请求参数错误 | +| 103 | 用户名或密码错误 | +| 104 | 请求方法错误 | +| 105 | 未知错误 | + +## 获取所有 api 接口列表 + +### GET /api/v1/endpoints + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- |---------|-------------| +| code | Integer | 1 | +| data | Array | 接口列表 | +| method | String | 方法名 | +| path | String | 路径 | + +**Examples:** + +```bash +$ curl -i --basic -u mica:mica "http://localhost:18083/api/v1/endpoints" + +{ + "data": [ + { + "method": "GET", + "path": "/api/v1/endpoints" + }, + { + "method": "GET", + "path": "/api/v1/clients" + }, + { + "method": "POST", + "path": "/api/v1/mqtt/unsubscribe" + }, + { + "method": "GET", + "path": "/api/v1/clients/info" + }, + { + "method": "GET", + "path": "/api/v1/stats" + }, + { + "method": "GET", + "path": "/api/v1/stats/sse" + }, + { + "method": "POST", + "path": "/api/v1/mqtt/publish/batch" + }, + { + "method": "POST", + "path": "/api/v1/mqtt/subscribe/batch" + }, + { + "method": "GET", + "path": "/api/v1/client/subscriptions" + }, + { + "method": "POST", + "path": "/api/v1/mqtt/publish" + }, + { + "method": "POST", + "path": "/api/v1/mqtt/unsubscribe/batch" + }, + { + "method": "POST", + "path": "/api/v1/mqtt/subscribe" + }, + { + "method": "POST", + "path": "/api/v1/clients/delete" + } + ], + "code": 1 +} +``` + +## 消息发布 + +### POST /api/v1/mqtt/publish + +发布 MQTT 消息。 + +**Parameters (json):** + +| Name | Type | Required | Default | Description | +| -------- | ------- | -------- | ------- |------------------------------------------------| +| topic | String | Required | | 主题,消息会按 topic 订阅投递 | +| clientId | String | Required | | 客户端标识符,不为空参数即可,无实际意义,建议可以取名 httpApi | +| payload | String | Required | | 消息正文 | +| encoding | String | Optional | plain | 消息正文使用的编码方式,目前仅支持 目前仅支持 `plain`、`hex`、`base64` | +| qos | Integer | Optional | 0 | QoS 等级 | +| retain | Boolean | Optional | false | 是否为保留消息 | + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- | ------- | ----------- | +| code | Integer | 0 | + +**Examples:** + +```bash +$ curl -i --basic -u mica:mica -X POST "http://localhost:18083/api/v1/mqtt/publish" -d '{"topic":"a/b/c","payload":"Hello World","qos":1,"retain":false,"clientId":"example"}' + +{"code":1} +``` + +## 主题订阅 + +### POST /api/v1/mqtt/subscribe + +订阅 MQTT 主题。 + +**Parameters (json):** + +| Name | Type | Required | Default | Description | +| -------- | ------- | -------- | ------- | ----------------------------------------------------- | +| topic | String | Required | | 主题 | +| clientId | String | Required | | 客户端标识符 | +| qos | Integer | Optional | 0 | QoS 等级 | + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- | ------- | ----------- | +| code | Integer | 0 | + +**Examples:** + +同时订阅 `a`, `b`, `c` 三个主题 + +```bash +$ curl -i --basic -u mica:mica -X POST "http://localhost:18083/api/v1/mqtt/subscribe" -d '{"topic":"a/b/c","qos":1,"clientId":"example"}' + +{"code":1} +``` + +### POST /api/v1/mqtt/unsubscribe + +取消订阅。 + +**Parameters (json):** + +| Name | Type | Required | Default | Description | +| -------- | ------ | -------- | ------- | ------------ | +| topic | String | Required | | 主题 | +| clientId | String | Required | | 客户端标识符 | + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- | ------- | ----------- | +| code | Integer | 0 | + +**Examples:** + +取消订阅 `a` 主题 + +```bash +$ curl -i --basic -u mica:mica -X POST "http://localhost:18083/api/v1/mqtt/unsubscribe" -d '{"topic":"a","clientId":"example"}' + +{"code":1} +``` + +## 消息批量发布 + +### POST /api/v1/mqtt/publish/batch + +批量发布 MQTT 消息。 + +**Parameters (json):** + +| Name | Type | Required | Default | Description | +| ------------ | ------- | -------- | ------- |------------------------------------------| +| [0].topic | String | Required | | 主题,消息按订阅投递 | +| [0].clientId | String | Required | | 客户端标识符,不为空参数即可,无实际意义,建议可以取名 httpApi | +| [0].payload | String | Required | | 消息正文 | +| [0].encoding | String | Optional | plain | 消息正文使用的编码方式,目前仅支持 `plain`、`hex`、`base64` | +| [0].qos | Integer | Optional | 0 | QoS 等级 | +| [0].retain | Boolean | Optional | false | 是否为保留消息 | + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- | ------- | ----------- | +| code | Integer | 0 | + +**Examples:** + +```bash +$ curl -i --basic -u mica:mica -X POST "http://localhost:18083/api/v1/mqtt/publish/batch" -d '[{"topic":"a/b/c","payload":"Hello World","qos":1,"retain":false,"clientId":"example"},{"topic":"a/b/c","payload":"Hello World Again","qos":0,"retain":false,"clientId":"example"}]' + +{"code":1} +``` + +## 主题批量订阅 + +### POST /api/v1/mqtt/subscribe/batch + +批量订阅 MQTT 主题。 + +**Parameters (json):** + +| Name | Type | Required | Default | Description | +| ------------ | ------- | -------- | ------- | ----------------------------------------------------- | +| [0].topic | String | Required | | 主题 | +| [0].clientId | String | Required | | 客户端标识符 | +| [0].qos | Integer | Optional | 0 | QoS 等级 | + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- | ------- | ----------- | +| code | Integer | 0 | + +**Examples:** + +一次性订阅 `a`, `b`, `c` 三个主题 + +```bash +$ curl -i --basic -u mica:mica -X POST "http://localhost:18083/api/v1/mqtt/subscribe/batch" -d '[{"topic":"a","qos":1,"clientId":"example"},{"topic":"b","qos":1,"clientId":"example"},{"topic":"c","qos":1,"clientId":"example"}]' + +{"code":1} +``` + +### POST /api/v1/mqtt/unsubscribe/batch + +批量取消订阅。 + +**Parameters (json):** + +| Name | Type | Required | Default | Description | +| ------------ | ------ | -------- | ------- | ------------ | +| [0].topic | String | Required | | 主题 | +| [0].clientId | String | Required | | 客户端标识符 | + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- | ------- | ----------- | +| code | Integer | 0 | + +**Examples:** + +一次性取消订阅 `a`, `b` 主题 + +```bash +$ curl -i --basic -u mica:mica -X POST "http://localhost:18083/api/v1/mqtt/unsubscribe/batch" -d '[{"topic":"a","clientId":"example"},{"topic":"b","clientId":"example"}]' + +{"code":1} +``` + +## 获取客户端详情 + +### GET /api/v1/clients/info + +**Query Parameters:** + +| Name | Type | Required | Description | +| -------- | ------ | -------- | ----------- | +| clientId | String | True | ClientID | + +**Success Response Body (JSON):** + +| Name | Type | Description | +|-----------|---------|-------------| +| code | Integer | 0 | +| clientId | String | clientId | +| username | String | 用户名 | +| connected | Boolen | 是否已经连接 | +| createdAt | Long | 连接的时间 | +| connectedAt | Long | 连接成功时间 | + +**Examples:** + +```bash +$ curl -i --basic -u mica:mica -X POST "http://localhost:18083/api/v1/clients/info?clientId=mqttx_5fe4cfcf" + +{"code":1,"data":{"clientId":"mqttx_5fe4cfcf","connected":true,"connectedAt":1681792417835,"createdAt":1681792417835,"ipAddress":"127.0.0.1","port":11852,"protoName":"MQTT","protoVer":5}} +``` + +## 分页获取客户端 + +### GET /api/v1/clients + +**Query Parameters:** + +| Name | Type | Required | Description | +|--------|------|----------|--------------| +| _page | int | False | Page 默认1 | +| _limit | int | False | 分页大小 默认10000 | + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- | ------- | ----------- | +| code | Integer | 0 | + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- | ------- |-------------| +| code | Integer | 0 | +| pageNumber | Integer | 当前页码 | +| pageSize | Integer | 分页大小 | +| totalRow | Integer | 分页数 | +| clientId | String | clientId | +| username | String | 用户名 | +| connected | Boolen | 是否已经连接 | +| createdAt | Long | 连接的时间 | +| connectedAt | Long | 连接成功时间 | + +**Examples:** + +```bash +$ curl -i --basic -u mica:mica -X POST "http://localhost:18083/api/v1/clients?_page=1&_limit=100" + +{"data":{"list":[{"clientId":"mqttx_5fe4cfcf","connected":true,"protoName":"MQTT","protoVer":5,"ipAddress":"127.0.0.1","port":11852,"connectedAt":1681792417835,"createdAt":1681792417835}],"pageNumber":1,"pageSize":1,"totalRow":1},"code":1} +``` + +## 踢出指定客户端 + +### POST /api/v1/clients/delete + +踢出指定客户端。注意踢出客户端操作会将连接与会话一并终结。 + +**Query Parameters:** + +| Name | Type | Required | Description | +| -------- | ------ | -------- | ----------- | +| clientId | String | True | ClientID | + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- | ------- | ----------- | +| code | Integer | 0 | + +**Examples:** + +由于客户端可能会重连,所以还会连上了。如果需要永久踢出需要自行开发黑名单。 + +```bash +$ curl -i --basic -u mica:mica -X POST "http://localhost:18083/api/v1/clients/delete?clientId=123" + +{"code":1} +``` + +## 获取客户端订阅情况 + +### GET /api/v1/client/subscriptions + +获取指定客户端订阅详情。 + +**Query Parameters:** + +| Name | Type | Required | Description | +| -------- | ------ | -------- | ----------- | +| clientId | String | True | ClientID | + +**Success Response Body (JSON):** + +| Name | Type | Description | +| ---- |---------|-------------| +| code | Integer | 0 | +| data | Array | [] | +| topicFilter | String | | +| clientId | String | | +| mqttQoS | Integer | 0 | + +**Examples:** + +```bash +$ curl -i --basic -u mica:mica "http://127.0.0.1:8083/api/v1/client/subscriptions?clientId=123" + +{ + "code": 1, + "data": [ + { + "clientId": "123", + "mqttQoS": 0, + "topicFilter": "#" + }, + { + "clientId": "123", + "mqttQoS": 0, + "topicFilter": "testtopic/#" + } + ] +} +``` \ No newline at end of file diff --git a/docs/http/http-client.env.json b/docs/http/http-client.env.json new file mode 100644 index 0000000..62b7f9d --- /dev/null +++ b/docs/http/http-client.env.json @@ -0,0 +1,7 @@ +{ + "local": { + "host": "127.0.0.1:18083", + "username": "mica", + "password": "mica" + } +} diff --git a/docs/http/mica-mqtt-api.http b/docs/http/mica-mqtt-api.http new file mode 100644 index 0000000..608d3b8 --- /dev/null +++ b/docs/http/mica-mqtt-api.http @@ -0,0 +1,132 @@ +### mqtt endpoints +GET http://{{host}}/api/v1/endpoints +Content-Type: application/json +Authorization: Basic {{username}} {{password}} + +### mqtt clients +GET http://{{host}}/api/v1/clients +Content-Type: application/json +Authorization: Basic {{username}} {{password}} + +### mqtt client info +GET http://{{host}}/api/v1/clients/info?clientId=123 +Content-Type: application/json +Authorization: Basic {{username}} {{password}} + +### mqtt stats +GET http://{{host}}/api/v1/stats +Content-Type: application/json +Authorization: Basic {{username}} {{password}} + +### mqtt stats sse +GET http://{{host}}/api/v1/stats/sse +Authorization: Basic {{username}} {{password}} + +### mqtt publish +POST http://{{host}}/api/v1/mqtt/publish +Content-Type: application/json +Authorization: Basic {{username}} {{password}} + +{ + "topic":"a/b/c", + "payload":"Hello World", + "qos":1, + "retain":false, + "clientId":"example" +} + +### mqtt subscribe +POST http://{{host}}/api/v1/mqtt/subscribe +Content-Type: application/json +Authorization: Basic {{username}} {{password}} + +{ + "topic":"a/b/c", + "qos":1, + "clientId":"example" +} + +### mqtt unsubscribe +POST http://{{host}}/api/v1/mqtt/unsubscribe +Content-Type: application/json +Authorization: Basic {{username}} {{password}} + +{ + "topic":"a/b/c", + "clientId":"example" +} + +### mqtt publish batch +POST http://{{host}}/api/v1/mqtt/publish/batch +Content-Type: application/json +Authorization: Basic {{username}} {{password}} + +[ + { + "topic":"a/b/c", + "payload":"Hello World", + "qos":1, + "retain":false, + "clientId":"example" + }, + { + "topic":"a/b/c", + "payload":"Hello World Again", + "qos":0, + "retain":false, + "clientId":"example" + } +] + +### mqtt subscribe batch +POST http://{{host}}/api/v1/mqtt/subscribe/batch +Content-Type: application/json +Authorization: Basic {{username}} {{password}} + +[ + { + "topic":"a", + "qos":1, + "clientId":"example" + }, + { + "topic":"b", + "qos":1, + "clientId":"example" + }, + { + "topic":"c", + "qos":1, + "clientId":"example" + } +] + +### mqtt unsubscribe batch +POST http://{{host}}/api/v1/mqtt/unsubscribe/batch +Content-Type: application/json +Authorization: Basic {{username}} {{password}} + +[ + { + "topic":"a", + "clientId":"example" + }, + { + "topic":"b", + "clientId":"example" + } +] + +### mqtt delete clients +POST http://{{host}}/api/v1/clients/delete +Content-Type: application/x-www-form-urlencoded +Authorization: Basic {{username}} {{password}} + +clientId=123 + +### mqtt client subscriptions +GET http://{{host}}/api/v1/client/subscriptions +Content-Type: application/x-www-form-urlencoded +Authorization: Basic {{username}} {{password}} + +clientId=123 diff --git a/docs/img/dreamlu-weixin.jpg b/docs/img/dreamlu-weixin.jpg new file mode 100644 index 0000000..cba946c Binary files /dev/null and b/docs/img/dreamlu-weixin.jpg differ diff --git a/docs/img/mica-mqtt.jpg b/docs/img/mica-mqtt.jpg new file mode 100644 index 0000000..4e65df3 Binary files /dev/null and b/docs/img/mica-mqtt.jpg differ diff --git a/docs/update.md b/docs/update.md new file mode 100644 index 0000000..83d59fa --- /dev/null +++ b/docs/update.md @@ -0,0 +1,91 @@ +# 升级指南 + +**mica-mqtt** 尽量减少对 api 的改动已保证老版本的平滑升级,但是有些大版本不得不改动。希望此文档对大家有所帮助。 + +## 迁移到 mica-mqtt 2.4.2 + +注意:2.4.2 将 MqttServerCustomizer 和 MqttClientCustomizer 抽到 mica-mqtt-server、mica-mqtt-client。Spring Boot 和 Solon 插入如果有使用到,请先将老的包导入删除,idea 会自动引入新的包。 + +**客户端替换包导入:** +- 替换成 `import org.dromara.mica.mqtt.core.client.MqttClientCustomizer;` + +**服务端替换包导入:** +- 替换成 `import org.dromara.mica.mqtt.core.server.MqttServerCustomizer;` + +## 迁移到 mica-mqtt 2.4.x 以上版本 + +- :truck: 调整 maven groupId `net.dreamlu` 到新的 `org.dromara.mica-mqtt`。 +- :truck: 调整包名 `net.dreamlu.iot.mqtt` 到新的 `org.dromara.mica.mqtt`,其他均保持不变。 +- :truck: 切换到 central sonatype,central sonatype 不支持快照版,mica-mqtt 不再发布快照版。 + +## 迁移到 mica-mqtt 2.1.x + +- `mica-mqtt-core` 拆分成了 `mica-mqtt-client` 和 `mica-mqtt-server`,避免一些依赖引用问题。 +- `ByteBufferUtil` 由 `org.dromara.mica.mqtt.codec.ByteBufferUtil` 移动到了 `org.tio.utils.buffer.ByteBufferUtil`。 +- `HexUtil` 由 `org.dromara.mica.mqtt.core.util.HexUtil` 移动到了 `org.tio.utils.mica.HexUtils`。 + +### 1. 客户端 + +#### 1.1 订阅回调接口调整 +注意:`mica-mqtt-client-spring-boot-starter` 使用注解订阅可以直升。 + +`IMqttClientMessageListener#onMessage(ChannelContext context, String topic, MqttPublishMessage message, ByteBuffer payload)` 方法统一添加 `context`、`message` 参数。 + +订阅系列方法需要调整: +```java +// 消息订阅,同类方法 subxxx +client.subQos0("/test/#", (context, topic, message, payload) -> { + logger.info(topic + '\t' + ByteBufferUtil.toString(payload)); +}); +``` + +#### 1.2 SSL 双向认证支持 +```yaml +mica: + client: + ssl: + enabled: false # 是否开启 ssl 认证,2.1.0 开始支持双向认证 + keystore-path: # 可选参数:ssl 双向认证 keystore 目录,支持 classpath:/ 路径。 + keystore-pass: # 可选参数:ssl 双向认证 keystore 密码 + truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 + truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 +``` + +注意: ssl 存在三种情况 + +| 服务端开启ssl | 客户端 | +| ---------------------------------------- | --------------------------------------------- | +| ClientAuth 为 NONE(不需要客户端验证) | 仅仅需要开启 ssl 即可不用配置证书 | +| ClientAuth 为 OPTIONAL(与客户端协商) | 需开启 ssl 并且配置 truststore 证书 | +| ClientAuth 为 REQUIRE (必须的客户端验证) | 需开启 ssl 并且配置 truststore、 keystore证书 | + +### 2. 服务端 + +#### 2.1 IMqttMessageListener 调整 + +`IMqttMessageListener` onMessage 参数也做了调整,添加了 topic、qoS,message 改为了原始 MqttPublishMessage,方便自行获取 mqtt5.x 的属性。 +```java +/** + * 监听到消息 + * + * @param context ChannelContext + * @param clientId clientId + * @param topic topic + * @param qoS MqttQoS + * @param message Message + */ +void onMessage(ChannelContext context, String clientId, String topic, MqttQoS qoS, MqttPublishMessage message); +``` + +#### 2.2 ssl 双向认证支持 +```yaml +mica: + server: + ssl: # mqtt tcp ssl 认证 + enabled: false # 是否开启 ssl 认证,2.1.0 开始支持双向认证 + keystore-path: # 必须参数:ssl keystore 目录,支持 classpath:/ 路径。 + keystore-pass: # 必选参数:ssl keystore 密码 + truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 + truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 + client-auth: none # 是否需要客户端认证(双向认证),默认:NONE(不需要) +``` \ No newline at end of file diff --git a/mica-mqtt-client/README.md b/mica-mqtt-client/README.md new file mode 100644 index 0000000..b959630 --- /dev/null +++ b/mica-mqtt-client/README.md @@ -0,0 +1,157 @@ +# 使用文档 + +## topic 通配符含义 +- `/`:用来表示层次,比如 a/b,a/b/c。 +- `#`:表示匹配 `>=0` 个层次,比如 a/# 就匹配 a/,a/b,a/b/c。单独的一个 # 表示匹配所有。不允许 a# 和 a/#/c。 +- `+`:表示匹配一个层次,例如 a/+ 匹配 a/b,a/c,不匹配 a/b/c。单独的一个 + 是允许的,a+ 不允许,也可以和多层通配符一起使用,+/tennis/# 、sport/+/player1 都有有效的。 + +## 使用说明 + +### MQTT 遗嘱消息场景 + +- 当客户端断开连接时,发送给相关的订阅者的遗嘱消息。在设备 A 进行连接时候,遗嘱消息设定为 `offline`,手机App B 订阅这个遗嘱主题。 +- 当 A 异常断开时,手机App B 会收到这个 `offline` 的遗嘱消息,从而知道设备 A 离线了。 + +### MQTT 保留消息场景 + +- 例如,某设备定期发布自身 GPS 坐标,但对于订阅者而言,从它发起订阅到第一次收到数据可能需要几秒钟,也可能需要十几分钟甚至更多,这样并不友好。因此 MQTT 引入了保留消息。 +- 而每当有订阅者建立订阅时,服务端就会查找是否存在匹配该订阅的保留消息,如果保留消息存在,就会立即转发给订阅者。 +- 借助保留消息,新的订阅者能够立即获取最近的状态。 + +### 共享订阅 +mica-mqtt 支持两种**共享订阅**方式: + +1. 共享订阅:订阅前缀 `$queue/`,多个客户端订阅了 `$queue/topic`,发布者发布到 `topic`,则只有一个客户端会接收到消息。 +2. 分组订阅:订阅前缀 `$share//`,组客户端订阅了 `$share/group1/topic`、`$share/group2/topic`..,发布者发布到 `topic`,则消息会发布到每个 **group** 中,但是每个 **group** 中只有一个客户端会接收到消息。 + +**注意:** 如果发布的 `topic` 以 `/` 开头,例如:`/topic/test`,需要订阅 `$share/group1//topic/test`,另外 mica-mqtt 默认随机消息路由,共享订阅的多个客户端会随机收到消息。 + +## 客户端使用 + +### 添加依赖 + +```xml + + org.dromara.mica-mqtt + mica-mqtt-client + ${mica-mqtt.version} + +``` + +## 客户端使用 + +```java +// 初始化 mqtt 客户端 +MqttClient client = MqttClient.create() + .ip("127.0.0.1") // mqtt 服务端 ip 地址 + .port(1883) // 默认:1883 + .username("admin") // 账号 + .password("123456") // 密码 + .version(MqttVersion.MQTT_5) // 默认:3_1_1 + .clientId("xxxxxx") // 非常重要务必手动设置,一般设备 sn 号,默认:MICA-MQTT- 前缀和 36进制的纳秒数 + .readBufferSize(512) // 消息一起解析的长度,默认:为 8092 (mqtt 消息最大长度) + .maxBytesInMessage(1024 * 10) // 最大包体长度,如果包体过大需要设置此参数,默认为: 10M (10*1024*1024) + .keepAliveSecs(120) // 默认:60s + .timeout(10) // 超时时间,t-io 配置,可为 null,为 null 时,t-io 默认为 5 + .reconnect(true) // 是否重连,默认:true + .reInterval(5000) // 重连重试时间,reconnect 为 true 时有效,t-io 默认为:5000 + .willMessage(builder -> { + builder.topic("/test/offline").messageText("down"); // 遗嘱消息 + }) + .connectListener(new IMqttClientConnectListener() { + @Override + public void onConnected(ChannelContext context, boolean isReconnect) { + logger.info("链接服务器成功..."); + } + + @Override + public void onDisconnect(ChannelContext channelContext, Throwable throwable, String remark, boolean isRemove) { + logger.info("与链接服务器断开连接..."); + } + }) + .properties() // mqtt5 properties + .connectSync(); // 同步连接,也可以使用 connect(),可以避免 broker 没启动照成启动卡住。 + + // 消息订阅,同类方法 subxxx + client.subQos0("/test/#", (context, topic, message, payload) -> { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + }); + // 取消订阅 + client.unSubscribe("/test/#"); + + // 发送消息 + client.publish("/test/client", "mica最牛皮".getBytes(StandardCharsets.UTF_8)); + + // 断开连接 + client.disconnect(); + // 重连 + client.reconnect(); + // 停止 + client.stop(); +``` + +## 在 Android 中使用 + +### 排除 INDEX.LIST 文件 +```groovy +android { + // ... 其他配置 + packagingOptions { + // 排除 INDEX.LIST 文件 + exclude 'META-INF/INDEX.LIST' + } +} +``` + +### 添加依赖 + +```groovy +implementation 'org.dromara.mica-mqtt:mica-mqtt-client:${micaMqttVersion}' // 使用 2.4.2 或以上版本 +``` + +## 全局订阅(2.2.9开始支持) +**说明**:由于 mica-mqtt-client 采用传统 mq 的思维进行的开发。其实是跟 mqtt 部分是有违背的。传统 mqtt client 不会按 topic 进行不通的订阅,采用的是这里的**全局订阅**方式。 +**注意**:全局订阅也是可以监听到 `subQos0`、`subQos1`、`subQos2` 的消息。采用 `globalSubscribe`,保留 session 停机重启,依然可以接受到消息。 +```java +// 初始化 mqtt 客户端 +MqttClient.create() + .ip("127.0.0.1") + .port(1883) + .username("admin") + .password("123456") + // 采用 globalSubscribe,保留 session 停机重启后,可以接受到离线消息,注意:clientId 要不能变化。 + .clientId("globalTest") + .cleanSession(false) + // 全局订阅的 topic + .globalSubscribe("/test", "/test/123", "/debug/#") + // 全局监听,也会监听到服务端 http api 订阅的数据 + .globalMessageListener((context, topic, message, payload) -> { + System.out.println("topic:\t" + topic); + System.out.println("payload:\t" + ByteBufferUtil.toString(payload)); + }) + .connectSync(); +``` + +## 接口代理 + +```java +// 初始化 mqtt 客户端 +MqttClient client = MqttClient.create() + .ip("127.0.0.1") + .port(1883) + .username("admin") + .password("123456") + .connectSync(); +// 代理接口 +DoorClient doorClient = client.getInterface(DoorClient.class); + +client.schedule(() -> { + doorClient.sendMessage("open", false); +}, 1000); + +public interface DoorClient { + + @MqttClientPublish(value = "/a/door/open", qos = MqttQoS.QOS0) + void sendMessage(@MqttPayload String message, @MqttRetain boolean retain); +} +``` diff --git a/mica-mqtt-client/pom.xml b/mica-mqtt-client/pom.xml new file mode 100644 index 0000000..e717f38 --- /dev/null +++ b/mica-mqtt-client/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + mica-mqtt + ${revision} + + mica-mqtt-client + ${project.artifactId} + https://mica-mqtt.dreamlu.net/guide/java/client.html + + + + org.dromara.mica-mqtt + mica-mqtt-common + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + org.moditect + moditect-maven-plugin + + + + + diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/DefaultMqttClientProcessor.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/DefaultMqttClientProcessor.java new file mode 100644 index 0000000..fd14d85 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/DefaultMqttClientProcessor.java @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.codes.MqttConnectReasonCode; +import org.dromara.mica.mqtt.codec.message.*; +import org.dromara.mica.mqtt.codec.message.builder.MqttTopicSubscription; +import org.dromara.mica.mqtt.codec.message.header.MqttConnAckVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttPublishVariableHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttSubAckPayload; +import org.dromara.mica.mqtt.core.common.MqttPendingPublish; +import org.dromara.mica.mqtt.core.common.MqttPendingQos2Publish; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Node; +import org.tio.core.Tio; +import org.tio.utils.hutool.CollUtil; +import org.tio.utils.timer.TimerTaskService; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; + +/** + * 默认的 mqtt 消息处理器 + * + * @author L.cm + */ +public class DefaultMqttClientProcessor implements IMqttClientProcessor { + private static final Logger logger = LoggerFactory.getLogger(DefaultMqttClientProcessor.class); + private final MqttClientCreator mqttClientCreator; + private final IMqttClientSession clientSession; + private final IMqttClientConnectListener connectListener; + private final IMqttClientGlobalMessageListener globalMessageListener; + private final TimerTaskService taskService; + private final ExecutorService executor; + + public DefaultMqttClientProcessor(MqttClientCreator mqttClientCreator) { + this.mqttClientCreator = mqttClientCreator; + this.clientSession = mqttClientCreator.getClientSession(); + this.connectListener = mqttClientCreator.getConnectListener(); + this.globalMessageListener = mqttClientCreator.getGlobalMessageListener(); + this.taskService = mqttClientCreator.getTaskService(); + this.executor = mqttClientCreator.getMqttExecutor(); + } + + @Override + public void processConAck(ChannelContext context, MqttConnAckMessage message) { + MqttConnAckVariableHeader connAckVariableHeader = message.variableHeader(); + MqttConnectReasonCode returnCode = connAckVariableHeader.connectReturnCode(); + switch (returnCode) { + case CONNECTION_ACCEPTED: + // 1. 连接成功的日志 + context.setAccepted(true); + if (logger.isInfoEnabled()) { + Node node = context.getServerNode(); + logger.info("MqttClient contextId:{} connection:{}:{} succeeded!", context.getId(), node.getIp(), node.getPort()); + } + // 2. 发布连接通知 + publishConnectEvent(context); + // 3. 发送订阅,不管服务端是否存在 session 都发送 + reSendSubscription(context); + break; + case CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD: + case CONNECTION_REFUSED_IDENTIFIER_REJECTED: + case CONNECTION_REFUSED_NOT_AUTHORIZED: + case CONNECTION_REFUSED_SERVER_UNAVAILABLE: + case CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION: + default: + String remark = "MqttClient connect error error ReturnCode:" + returnCode; + Tio.close(context, remark); + break; + } + } + + /** + * 发布连接成功事件 + * + * @param context ChannelContext + */ + private void publishConnectEvent(ChannelContext context) { + // 先判断是否配置监听 + if (connectListener == null) { + return; + } + // 触发客户端连接事件 + executor.submit(() -> { + try { + connectListener.onConnected(context, context.isReconnect()); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + }); + } + + /** + * 批量重新订阅 + * + * @param context ChannelContext + */ + private void reSendSubscription(ChannelContext context) { + // 0. 全局订阅 + Set globalSubscribe = mqttClientCreator.getGlobalSubscribe(); + if (globalSubscribe != null && !globalSubscribe.isEmpty()) { + globalReSendSubscription(context, globalSubscribe); + } + List reSubscriptionList = clientSession.getSubscriptions(); + // 1. 判断是否为空 + if (reSubscriptionList.isEmpty()) { + return; + } + // 2. 订阅的数量 + int subscribedSize = reSubscriptionList.size(); + // 重新订阅批次大小 + int reSubscribeBatchSize = mqttClientCreator.getReSubscribeBatchSize(); + if (subscribedSize <= reSubscribeBatchSize) { + reSendSubscription(context, reSubscriptionList); + } else { + List> partitionList = CollUtil.partition(reSubscriptionList, reSubscribeBatchSize); + for (List partition : partitionList) { + reSendSubscription(context, partition); + } + } + } + + /** + * 全局订阅,不需要存储 session + * + * @param context ChannelContext + * @param globalReSubscriptionList globalReSubscriptionList + */ + private void globalReSendSubscription(ChannelContext context, Set globalReSubscriptionList) { + int packetId = clientSession.getPacketId(); + MqttSubscribeMessage message = MqttSubscribeMessage.builder() + .addSubscriptions(globalReSubscriptionList) + .messageId(packetId) + .build(); + boolean result = Tio.send(context, message); + logger.info("MQTT globalReSubscriptionList:{} packetId:{} resubscribing result:{}", globalReSubscriptionList, packetId, result); + } + + /** + * 批量重新订阅 + * + * @param context ChannelContext + * @param reSubscriptionList reSubscriptionList + */ + private void reSendSubscription(ChannelContext context, List reSubscriptionList) { + // 2. 批量重新订阅 + List topicSubscriptionList = reSubscriptionList.stream() + .map(MqttClientSubscription::toTopicSubscription) + .collect(Collectors.toList()); + int packetId = clientSession.getPacketId(); + MqttSubscribeMessage message = MqttSubscribeMessage.builder() + .addSubscriptions(topicSubscriptionList) + .messageId(packetId) + .build(); + MqttPendingSubscription pendingSubscription = new MqttPendingSubscription(reSubscriptionList, message); + pendingSubscription.startRetransmitTimer(taskService, context); + clientSession.addPaddingSubscribe(packetId, pendingSubscription); + // gitee issues #IB72L6 先添加并启动重试,再发送订阅 + boolean result = Tio.send(context, message); + logger.info("MQTT subscriptionList:{} packetId:{} resubscribing result:{}", reSubscriptionList, packetId, result); + } + + @Override + public void processSubAck(ChannelContext context, MqttSubAckMessage message) { + int packetId = message.variableHeader().messageId(); + logger.debug("MqttClient SubAck packetId:{}", packetId); + MqttPendingSubscription paddingSubscribe = clientSession.getPaddingSubscribe(packetId); + if (paddingSubscribe == null) { + return; + } + List subscriptionList = paddingSubscribe.getSubscriptionList(); + MqttSubAckPayload subAckPayload = message.payload(); + List reasonCodeList = subAckPayload.reasonCodes(); + // reasonCodes 为空 + if (reasonCodeList.isEmpty()) { + logger.error("MqttClient subscriptionList:{} subscribe failed reasonCodes is empty packetId:{}", subscriptionList, packetId); + return; + } + int reasonCodeListSize = reasonCodeList.size(); + // 找出订阅成功的数据 + List subscribedList = new ArrayList<>(); + // MQTT 3.1.1 协议未明确规定批量订阅的返回格式,批量可能只返回一个 reasonCode + if (reasonCodeListSize == 1) { + Short reasonCode = reasonCodeList.get(0); + // reasonCodes 范围,0 ~ 2 + if (reasonCode != null && reasonCode >= 0 && reasonCode <= 2) { + subscribedList.addAll(subscriptionList); + } + } else { + // MQTT 5.0 要求 Broker 对批量订阅中的每个主题返回独立的 reason code(原因码),与订阅请求中的主题顺序一一对应 + for (int i = 0; i < subscriptionList.size(); i++) { + MqttClientSubscription subscription = subscriptionList.get(i); + String topicFilter = subscription.getTopicFilter(); + Short reasonCode = reasonCodeList.get(i); + // reasonCodes 范围 + if (reasonCode == null || reasonCode < 0 || reasonCode > 2) { + logger.error("MqttClient topicFilter:{} subscribe failed reasonCodes:{} packetId:{}", topicFilter, reasonCode, packetId); + } else { + subscribedList.add(subscription); + } + } + } + // 判断订阅结果,对于没有订阅成功的,使其触发重试 + if (subscribedList.isEmpty()) { + logger.error("MqttClient subscriptionList:{} subscribe failed packetId:{}", subscriptionList, packetId); + return; + } else { + logger.info("MQTT subscribed:{} successfully packetId:{}", subscribedList, packetId); + } + paddingSubscribe.onSubAckReceived(); + clientSession.removePaddingSubscribe(packetId); + clientSession.addSubscriptionList(subscribedList); + // 触发已经监听的事件 + subscribedList.forEach(clientSubscription -> { + String topicFilter = clientSubscription.getTopicFilter(); + MqttQoS mqttQoS = clientSubscription.getMqttQoS(); + IMqttClientMessageListener subscriptionListener = clientSubscription.getListener(); + executor.execute(() -> { + try { + subscriptionListener.onSubscribed(context, topicFilter, mqttQoS, message); + } catch (Throwable e) { + logger.error("MQTT topicFilter:{} subscribed onSubscribed event error.", topicFilter, e); + } + }); + }); + } + + @Override + public void processPublish(ChannelContext context, MqttPublishMessage message) { + MqttFixedHeader mqttFixedHeader = message.fixedHeader(); + MqttPublishVariableHeader variableHeader = message.variableHeader(); + String topicName = variableHeader.topicName(); + MqttQoS mqttQoS = mqttFixedHeader.qosLevel(); + int packetId = variableHeader.packetId(); + logger.debug("MqttClient received publish topic:{} qoS:{} packetId:{}", topicName, mqttQoS, packetId); + switch (mqttFixedHeader.qosLevel()) { + case QOS0: + invokeListenerForPublish(context, topicName, message); + break; + case QOS1: + invokeListenerForPublish(context, topicName, message); + if (packetId != -1) { + MqttMessage messageAck = MqttPubAckMessage.builder() + .packetId(packetId) + .build(); + boolean resultPubAck = Tio.send(context, messageAck); + logger.debug("Publish - PubAck send topicName:{} mqttQoS:{} packetId:{} result:{}", topicName, mqttQoS, packetId, resultPubAck); + } + break; + case QOS2: + if (packetId != -1) { + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREC, false, MqttQoS.QOS0, false, 0); + MqttMessage pubRecMessage = new MqttMessage(fixedHeader, MqttMessageIdVariableHeader.from(packetId)); + MqttPendingQos2Publish pendingQos2Publish = new MqttPendingQos2Publish(message, pubRecMessage); + clientSession.addPendingQos2Publish(packetId, pendingQos2Publish); + pendingQos2Publish.startPubRecRetransmitTimer(taskService, context); + // 先启动重试再发布消息 + boolean resultPubRec = Tio.send(context, pubRecMessage); + logger.debug("Publish - PubRec send topicName:{} mqttQoS:{} packetId:{} result:{}", topicName, mqttQoS, packetId, resultPubRec); + } + break; + case FAILURE: + default: + } + } + + @Override + public void processUnSubAck(MqttUnSubAckMessage message) { + int packetId = message.variableHeader().messageId(); + logger.debug("MqttClient UnSubAck packetId:{}", packetId); + MqttPendingUnSubscription pendingUnSubscription = clientSession.getPaddingUnSubscribe(packetId); + if (pendingUnSubscription == null) { + return; + } + List unSubscriptionTopics = pendingUnSubscription.getTopics(); + logger.info("MQTT Topic:{} successfully unSubscribed packetId:{}", unSubscriptionTopics, packetId); + pendingUnSubscription.onUnSubAckReceived(); + clientSession.removePaddingUnSubscribe(packetId); + clientSession.removeSubscriptions(unSubscriptionTopics); + } + + @Override + public void processPubAck(MqttPubAckMessage message) { + int packetId = message.variableHeader().messageId(); + logger.debug("MqttClient PubAck packetId:{}", packetId); + MqttPendingPublish pendingPublish = clientSession.getPendingPublish(packetId); + if (pendingPublish == null) { + return; + } + if (logger.isInfoEnabled()) { + String topicName = pendingPublish.getMessage().variableHeader().topicName(); + logger.info("MQTT Topic:{} successfully PubAck packetId:{}", topicName, packetId); + } + pendingPublish.onPubAckReceived(); + clientSession.removePendingPublish(packetId); + } + + @Override + public void processPubRec(ChannelContext context, MqttMessage message) { + int packetId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId(); + logger.debug("MqttClient PubRec packetId:{}", packetId); + MqttPendingPublish pendingPublish = clientSession.getPendingPublish(packetId); + if (pendingPublish == null) { + return; + } + pendingPublish.onPubAckReceived(); + + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, MqttQoS.QOS1, false, 0); + MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) message.variableHeader(); + MqttMessage pubRelMessage = new MqttMessage(fixedHeader, variableHeader); + + pendingPublish.setPubRelMessage(pubRelMessage); + pendingPublish.startPubRelRetransmissionTimer(taskService, context); + + // 发送消息 + boolean result = Tio.send(context, pubRelMessage); + logger.debug("Publish - PubRec send packetId:{} result:{}", packetId, result); + } + + @Override + public void processPubRel(ChannelContext context, MqttMessage message) { + int packetId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId(); + logger.debug("MqttClient PubRel packetId:{}", packetId); + MqttPendingQos2Publish pendingQos2Publish = clientSession.getPendingQos2Publish(packetId); + if (pendingQos2Publish != null) { + MqttPublishMessage incomingPublish = pendingQos2Publish.getIncomingPublish(); + String topicName = incomingPublish.variableHeader().topicName(); + this.invokeListenerForPublish(context, topicName, incomingPublish); + pendingQos2Publish.onPubRelReceived(); + clientSession.removePendingQos2Publish(packetId); + } + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.QOS0, false, 0); + MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(packetId); + // 发送消息 + boolean result = Tio.send(context, new MqttMessage(fixedHeader, variableHeader)); + logger.debug("Publish - PubRel send packetId:{} result:{}", packetId, result); + } + + @Override + public void processPubComp(MqttMessage message) { + int packetId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId(); + MqttPendingPublish pendingPublish = clientSession.getPendingPublish(packetId); + if (pendingPublish == null) { + return; + } + if (logger.isInfoEnabled()) { + String topicName = pendingPublish.getMessage().variableHeader().topicName(); + logger.info("MQTT Topic:{} successfully PubComp", topicName); + } + pendingPublish.onPubCompReceived(); + clientSession.removePendingPublish(packetId); + } + + /** + * 处理订阅的消息 + * + * @param context ChannelContext + * @param topicName topicName + * @param message MqttPublishMessage + */ + private void invokeListenerForPublish(ChannelContext context, String topicName, MqttPublishMessage message) { + final byte[] payload = message.payload(); + // 全局消息监听器 + if (globalMessageListener != null) { + executor.submit(() -> { + try { + globalMessageListener.onMessage(context, topicName, message, payload); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + }); + } + // topic 订阅监听 + List subscriptionList = clientSession.getMatchedSubscription(topicName); + if (subscriptionList.isEmpty()) { + if (globalMessageListener == null || mqttClientCreator.isDebug()) { + logger.warn("Mqtt message to accept topic:{} subscriptionList is empty.", topicName); + } else { + logger.debug("Mqtt message to accept topic:{} subscriptionList is empty.", topicName); + } + } else { + subscriptionList.forEach(subscription -> { + IMqttClientMessageListener listener = subscription.getListener(); + executor.submit(() -> { + try { + listener.onMessage(context, topicName, message, payload); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + }); + }); + } + } + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/DefaultMqttClientSession.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/DefaultMqttClientSession.java new file mode 100644 index 0000000..3fc6f30 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/DefaultMqttClientSession.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.common.MqttPendingPublish; +import org.dromara.mica.mqtt.core.common.MqttPendingQos2Publish; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.utils.collection.IntObjectHashMap; +import org.tio.utils.collection.IntObjectMap; +import org.tio.utils.collection.MultiValueMap; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + * 客户端 session 管理,包括 sub 和 pub + * + * @author L.cm + */ +public class DefaultMqttClientSession implements IMqttClientSession { + private static final Logger logger = LoggerFactory.getLogger(DefaultMqttClientSession.class); + /** + * packetId 递增生成器 + */ + private final AtomicInteger packetIdGen = new AtomicInteger(1); + /** + * 订阅的数据承载 + */ + private final MultiValueMap subscriptions = new MultiValueMap<>(new ConcurrentHashMap<>()); + private final IntObjectMap pendingSubscriptions = new IntObjectHashMap<>(); + private final IntObjectMap pendingUnSubscriptions = new IntObjectHashMap<>(); + private final IntObjectMap pendingPublishData = new IntObjectHashMap<>(); + private final IntObjectMap pendingQos2PublishData = new IntObjectHashMap<>(); + + @Override + public int getPacketId() { + return packetIdGen.getAndUpdate(current -> (current % 0xffff) == 0 ? 1 : current + 1); + } + + @Override + public void addPaddingSubscribe(int messageId, MqttPendingSubscription pendingSubscription) { + pendingSubscriptions.put(messageId, pendingSubscription); + } + + @Override + public MqttPendingSubscription getPaddingSubscribe(int messageId) { + return pendingSubscriptions.get(messageId); + } + + @Override + public void removePaddingSubscribes(List topicFilters) { + Set needToRemove = new HashSet<>(); + pendingSubscriptions.forEach((messageId, pendingSubscription) -> { + List subscriptionList = pendingSubscription.getSubscriptionList(); + if (subscriptionList != null) { + subscriptionList.removeIf(subscription -> topicFilters.contains(subscription.getTopicFilter())); + } + // 如果已经被删到为空 + if (subscriptionList == null || subscriptionList.isEmpty()) { + // 停止线程 + pendingSubscription.onSubAckReceived(); + needToRemove.add(messageId); + } + }); + // 清除 messageId 的过程订阅 + needToRemove.forEach(pendingSubscriptions::remove); + } + + @Override + public MqttPendingSubscription removePaddingSubscribe(int messageId) { + return pendingSubscriptions.remove(messageId); + } + + @Override + public void addSubscription(MqttClientSubscription subscription) { + subscriptions.add(subscription.getTopicFilter(), subscription); + } + + @Override + public boolean isSubscribed(MqttClientSubscription clientSubscription) { + // 1. 判断是否已经存在订阅关系 + String topicFilter = clientSubscription.getTopicFilter(); + Set subscriptionSet = this.subscriptions.get(topicFilter); + if (subscriptionSet == null || subscriptionSet.isEmpty()) { + return false; + } + // 2. 存在时的逻辑 + MqttQoS mqttQoS = clientSubscription.getMqttQoS(); + IMqttClientMessageListener listener = clientSubscription.getListener(); + for (MqttClientSubscription subscription : subscriptionSet) { + // 3. 已经存在订阅 + if (clientSubscription.equals(subscription)) { + logger.error("MQTT Topic:{} mqttQoS:{} listener:{} duplicate subscription.", topicFilter, mqttQoS, listener); + return true; + } + MqttQoS subQos = subscription.getMqttQoS(); + IMqttClientMessageListener subListener = subscription.getListener(); + // 4. 如果已经存在更高或同级别 qos + if (subQos.value() >= mqttQoS.value()) { + // 5. 监听器不相同则直接添加 + if (subListener != listener) { + subscriptions.add(topicFilter, clientSubscription); + logger.warn("MQTT Topic:{} mqttQoS:{} listener:{} has a higher level qos, added directly.", topicFilter, mqttQoS, listener); + } else { + logger.error("MQTT Topic:{} mqttQoS:{} listener:{} has a higher level qos, duplicate subscription.", topicFilter, mqttQoS, listener); + } + return true; + } + } + return false; + } + + @Override + public List getSubscriptions() { + List subscriptionList = new ArrayList<>(); + for (Set mqttSubscriptions : subscriptions.values()) { + subscriptionList.addAll(mqttSubscriptions); + } + return Collections.unmodifiableList(subscriptionList); + } + + @Override + public List getMatchedSubscription(String topicName) { + return subscriptions.values().stream() + .flatMap(Collection::stream) + .filter(subscription -> subscription.matches(topicName)) + .collect(Collectors.toList()); + } + + @Override + public void removeSubscriptions(List topicFilters) { + topicFilters.forEach(subscriptions::remove); + } + + @Override + public void addPaddingUnSubscribe(int messageId, MqttPendingUnSubscription pendingUnSubscription) { + pendingUnSubscriptions.put(messageId, pendingUnSubscription); + } + + @Override + public MqttPendingUnSubscription getPaddingUnSubscribe(int messageId) { + return pendingUnSubscriptions.get(messageId); + } + + @Override + public MqttPendingUnSubscription removePaddingUnSubscribe(int messageId) { + return pendingUnSubscriptions.remove(messageId); + } + + @Override + public void addPendingPublish(int messageId, MqttPendingPublish pendingPublish) { + pendingPublishData.put(messageId, pendingPublish); + } + + @Override + public MqttPendingPublish getPendingPublish(int messageId) { + return pendingPublishData.get(messageId); + } + + @Override + public MqttPendingPublish removePendingPublish(int messageId) { + return pendingPublishData.remove(messageId); + } + + @Override + public void addPendingQos2Publish(int messageId, MqttPendingQos2Publish pendingQos2Publish) { + pendingQos2PublishData.put(messageId, pendingQos2Publish); + } + + @Override + public MqttPendingQos2Publish getPendingQos2Publish(int messageId) { + return pendingQos2PublishData.get(messageId); + } + + @Override + public MqttPendingQos2Publish removePendingQos2Publish(int messageId) { + return pendingQos2PublishData.remove(messageId); + } + + @Override + public void clean() { + subscriptions.clear(); + pendingSubscriptions.clear(); + pendingUnSubscriptions.clear(); + pendingPublishData.clear(); + pendingQos2PublishData.clear(); + } +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClient.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClient.java new file mode 100644 index 0000000..65b02b4 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClient.java @@ -0,0 +1,48 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import java.lang.reflect.Proxy; + +/** + * @author ChangJin Wei (魏昌进) + */ +public interface IMqttClient { + + /** + * 获取 mqtt 客户端 + * + * @return MqttClient + */ + MqttClient getMqttClient(); + + /** + * 增加一个代理接口方法 + * + * @param clientClass 被代理接口 + * @param 代理接口的类型 + * @return 代理对象 + */ + @SuppressWarnings("unchecked") + default T getInterface(Class clientClass) { + return (T) Proxy.newProxyInstance( + clientClass.getClassLoader(), + new Class[]{clientClass}, + new MqttInvocationHandler<>(this) + ); + } +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientConnectListener.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientConnectListener.java new file mode 100644 index 0000000..4a57271 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientConnectListener.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.tio.core.ChannelContext; + +/** + * mqtt 客户端连接监听 + * + * @author L.cm + */ +public interface IMqttClientConnectListener { + + /** + * 监听到消息 + * + * @param context ChannelContext + * @param isReconnect 是否重连 + */ + void onConnected(ChannelContext context, boolean isReconnect); + + /** + * 连接关闭前触发本方法 + * + * @param context the ChannelContext + * @param throwable the throwable 有可能为空 + * @param remark the remark 有可能为空 + * @param isRemove is removed + */ + void onDisconnect(ChannelContext context, Throwable throwable, String remark, boolean isRemove); + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientGlobalMessageListener.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientGlobalMessageListener.java new file mode 100644 index 0000000..8da5077 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientGlobalMessageListener.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.tio.core.ChannelContext; + +/** + * mqtt 全局消息处理 + * + * @author L.cm + */ +@FunctionalInterface +public interface IMqttClientGlobalMessageListener { + + /** + * 监听到消息 + * + * @param context ChannelContext + * @param topic topic + * @param message MqttPublishMessage + * @param payload payload + */ + void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload); + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientMessageListener.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientMessageListener.java new file mode 100644 index 0000000..9dce3a3 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientMessageListener.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttSubAckMessage; +import org.tio.core.ChannelContext; + +/** + * mqtt 消息处理 + * + * @author L.cm + */ +@FunctionalInterface +public interface IMqttClientMessageListener { + + /** + * 订阅成功之后的事件 + * + * @param context ChannelContext + * @param topicFilter topicFilter + * @param mqttQoS MqttQoS + * @param message MqttSubAckMessage + */ + default void onSubscribed(ChannelContext context, String topicFilter, MqttQoS mqttQoS, MqttSubAckMessage message) { + onSubscribed(context, topicFilter, mqttQoS); + } + + /** + * 订阅成功之后的事件 + * + * @param context ChannelContext + * @param topicFilter topicFilter + * @param mqttQoS MqttQoS + */ + default void onSubscribed(ChannelContext context, String topicFilter, MqttQoS mqttQoS) { + + } + + /** + * 监听到消息 + * + * @param context ChannelContext + * @param topic topic + * @param message MqttPublishMessage + * @param payload payload + */ + void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload); + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientProcessor.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientProcessor.java new file mode 100644 index 0000000..7ab7898 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientProcessor.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.message.*; +import org.tio.core.ChannelContext; + +/** + * mqtt 客户端消息处理器 + * + * @author L.cm + */ +public interface IMqttClientProcessor { + + /** + * 处理服务端链接 ack + * + * @param context ChannelContext + * @param message MqttConnAckMessage + */ + void processConAck(ChannelContext context, MqttConnAckMessage message); + + /** + * 处理服务端订阅的 ack + * + * @param message MqttSubAckMessage + * @param context ChannelContext + */ + void processSubAck(ChannelContext context, MqttSubAckMessage message); + + /** + * 处理服务端 publish 的消息 + * + * @param context ChannelContext + * @param message MqttPublishMessage + */ + void processPublish(ChannelContext context, MqttPublishMessage message); + + /** + * 处理服务端解除订阅的 ack + * + * @param message MqttSubAckMessage + */ + void processUnSubAck(MqttUnSubAckMessage message); + + /** + * 处理服务端 publish 的 ack + * + * @param message MqttPubAckMessage + */ + void processPubAck(MqttPubAckMessage message); + + /** + * 处理服务端 publish rec + * + * @param context ChannelContext + * @param message MqttPubAckMessage + */ + void processPubRec(ChannelContext context, MqttMessage message); + + /** + * 处理服务端 publish rel + * + * @param context ChannelContext + * @param message MqttPubAckMessage + */ + void processPubRel(ChannelContext context, MqttMessage message); + + /** + * 处理服务端 publish comp + * + * @param message MqttPubAckMessage + */ + void processPubComp(MqttMessage message); + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientSession.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientSession.java new file mode 100644 index 0000000..dff38db --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientSession.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.common.MqttPendingPublish; +import org.dromara.mica.mqtt.core.common.MqttPendingQos2Publish; + +import java.util.List; + +/** + * 客户端 session + * + * @author L.cm + */ +public interface IMqttClientSession { + + /** + * 获取 packetId + * + * @return packetId + */ + int getPacketId(); + + /** + * 添加订阅 + * + * @param messageId messageId + * @param pendingSubscription MqttPendingSubscription + */ + void addPaddingSubscribe(int messageId, MqttPendingSubscription pendingSubscription); + + /** + * 获取过程订阅 + * + * @param messageId messageId + * @return MqttPendingSubscription + */ + MqttPendingSubscription getPaddingSubscribe(int messageId); + + /** + * 移除过程订阅 + * + * @param topicFilters topicFilter 集合 + */ + void removePaddingSubscribes(List topicFilters); + + /** + * 删除过程订阅 + * + * @param messageId messageId + * @return MqttPendingSubscription + */ + MqttPendingSubscription removePaddingSubscribe(int messageId); + + /** + * 添加订阅 + * + * @param subscription MqttClientSubscription + */ + void addSubscription(MqttClientSubscription subscription); + + /** + * 添加启动时的临时订阅 + * + * @param topicFilters topicFilters + * @param qos MqttQoS + * @param messageListener IMqttClientMessageListener + */ + default void addSubscriptionList(String[] topicFilters, MqttQoS qos, IMqttClientMessageListener messageListener) { + for (String topicFilter : topicFilters) { + addSubscription(new MqttClientSubscription(qos, topicFilter, messageListener)); + } + } + + /** + * 添加订阅 + * + * @param subscriptionList MqttClientSubscription 集合 + */ + default void addSubscriptionList(List subscriptionList) { + for (MqttClientSubscription subscription : subscriptionList) { + addSubscription(subscription); + } + } + + /** + * 判断是否已经订阅过 + * + * @param clientSubscription MqttClientSubscription + * @return 是否已经订阅过 + */ + boolean isSubscribed(MqttClientSubscription clientSubscription); + + /** + * 获取并清除订阅 + * + * @return 订阅集合 + */ + List getSubscriptions(); + + /** + * 获取匹配的订阅 + * + * @param topicName topicName + * @return 订阅信息集合 + */ + List getMatchedSubscription(String topicName); + + /** + * 删除订阅过程消息 + * + * @param topicFilters topicFilter 集合 + */ + void removeSubscriptions(List topicFilters); + + /** + * 添加取消订阅过程消息 + * + * @param messageId messageId + * @param pendingUnSubscription MqttPendingUnSubscription + */ + void addPaddingUnSubscribe(int messageId, MqttPendingUnSubscription pendingUnSubscription); + + /** + * 获取取消订阅过程消息 + * + * @param messageId messageId + * @return MqttPendingUnSubscription + */ + MqttPendingUnSubscription getPaddingUnSubscribe(int messageId); + + /** + * 删除取消订阅过程消息 + * + * @param messageId messageId + * @return MqttPendingUnSubscription + */ + MqttPendingUnSubscription removePaddingUnSubscribe(int messageId); + + /** + * 添加过程消息 + * + * @param messageId messageId + * @param pendingPublish MqttPendingPublish + */ + void addPendingPublish(int messageId, MqttPendingPublish pendingPublish); + + /** + * 获取过程消息 + * + * @param messageId messageId + * @return MqttPendingPublish + */ + MqttPendingPublish getPendingPublish(int messageId); + + /** + * 删除过程消息 + * + * @param messageId messageId + * @return MqttPendingPublish + */ + MqttPendingPublish removePendingPublish(int messageId); + + /** + * 添加 qos2 过程消息 + * + * @param messageId messageId + * @param pendingQos2Publish MqttPendingQos2Publish + */ + void addPendingQos2Publish(int messageId, MqttPendingQos2Publish pendingQos2Publish); + + /** + * 获取 qos2 过程消息 + * + * @param messageId messageId + * @return MqttPendingQos2Publish + */ + MqttPendingQos2Publish getPendingQos2Publish(int messageId); + + /** + * 删除 qos2 过程消息 + * + * @param messageId messageId + * @return MqttPendingQos2Publish + */ + MqttPendingQos2Publish removePendingQos2Publish(int messageId); + + /** + * 资源清理 + */ + void clean(); + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClient.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClient.java new file mode 100644 index 0000000..90d1fa1 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClient.java @@ -0,0 +1,640 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.message.MqttSubscribeMessage; +import org.dromara.mica.mqtt.codec.message.MqttUnSubscribeMessage; +import org.dromara.mica.mqtt.codec.message.builder.MqttPublishBuilder; +import org.dromara.mica.mqtt.codec.message.builder.MqttTopicSubscription; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.core.common.MqttPendingPublish; +import org.dromara.mica.mqtt.core.serializer.MqttSerializer; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.client.ClientChannelContext; +import org.tio.client.TioClient; +import org.tio.client.TioClientConfig; +import org.tio.core.ChannelContext; +import org.tio.core.Node; +import org.tio.core.Tio; +import org.tio.utils.timer.TimerTask; +import org.tio.utils.timer.TimerTaskService; + +import java.util.*; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * mqtt 客户端 + * + * @author L.cm + * @author ChangJin Wei (魏昌进) + */ +public final class MqttClient implements IMqttClient { + private static final Logger logger = LoggerFactory.getLogger(MqttClient.class); + private final TioClient tioClient; + private final MqttClientCreator config; + private final TioClientConfig clientTioConfig; + private final IMqttClientSession clientSession; + private final TimerTaskService taskService; + private final ExecutorService mqttExecutor; + private final MqttSerializer mqttSerializer; + private ClientChannelContext context; + + public static MqttClientCreator create() { + return new MqttClientCreator(); + } + + MqttClient(TioClient tioClient, MqttClientCreator config) { + this.tioClient = tioClient; + this.config = config; + this.clientTioConfig = tioClient.getClientConfig(); + this.taskService = config.getTaskService(); + this.mqttExecutor = config.getMqttExecutor(); + this.clientSession = config.getClientSession(); + this.mqttSerializer = config.getMqttSerializer(); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subQos0(String topicFilter, IMqttClientMessageListener listener) { + return subscribe(topicFilter, MqttQoS.QOS0, listener); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subQos1(String topicFilter, IMqttClientMessageListener listener) { + return subscribe(topicFilter, MqttQoS.QOS1, listener); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subQos2(String topicFilter, IMqttClientMessageListener listener) { + return subscribe(topicFilter, MqttQoS.QOS2, listener); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subscribe(MqttQoS mqttQoS, String topicFilter, IMqttClientMessageListener listener) { + return subscribe(topicFilter, mqttQoS, listener, null); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subscribe(String topicFilter, MqttQoS mqttQoS, IMqttClientMessageListener listener) { + return subscribe(topicFilter, mqttQoS, listener, null); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @param properties MqttProperties + * @return MqttClient + */ + public MqttClient subscribe(String topicFilter, MqttQoS mqttQoS, IMqttClientMessageListener listener, MqttProperties properties) { + return subscribe(Collections.singletonList(new MqttClientSubscription(mqttQoS, topicFilter, listener)), properties); + } + + /** + * 订阅 + * + * @param topicFilters topicFilter 数组 + * @param mqttQoS MqttQoS + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subscribe(String[] topicFilters, MqttQoS mqttQoS, IMqttClientMessageListener listener) { + return subscribe(topicFilters, mqttQoS, listener, null); + } + + /** + * 订阅 + * + * @param topicFilters topicFilter 数组 + * @param mqttQoS MqttQoS + * @param listener MqttMessageListener + * @param properties MqttProperties + * @return MqttClient + */ + public MqttClient subscribe(String[] topicFilters, MqttQoS mqttQoS, IMqttClientMessageListener listener, MqttProperties properties) { + Objects.requireNonNull(topicFilters, "MQTT subscribe topicFilters is null."); + List subscriptionList = new ArrayList<>(); + for (String topicFilter : topicFilters) { + subscriptionList.add(new MqttClientSubscription(mqttQoS, topicFilter, listener)); + } + return subscribe(subscriptionList, properties); + } + + /** + * 批量订阅 + * + * @param subscriptionList 订阅集合 + * @return MqttClient + */ + public MqttClient subscribe(List subscriptionList) { + return subscribe(subscriptionList, null); + } + + /** + * 批量订阅 + * + * @param subscriptionList 订阅集合 + * @param properties MqttProperties + * @return MqttClient + */ + public MqttClient subscribe(List subscriptionList, MqttProperties properties) { + // 1. 先判断是否已经订阅过,重复订阅,直接跳出 + List needSubscriptionList = new ArrayList<>(); + for (MqttClientSubscription subscription : subscriptionList) { + // 校验 topicFilter + TopicUtil.validateTopicFilter(subscription.getTopicFilter()); + boolean subscribed = clientSession.isSubscribed(subscription); + if (!subscribed) { + needSubscriptionList.add(subscription); + } + } + // 2. 已经订阅的跳出 + if (needSubscriptionList.isEmpty()) { + return this; + } + List topicSubscriptionList = needSubscriptionList.stream() + .map(MqttClientSubscription::toTopicSubscription) + .collect(Collectors.toList()); + // 3. 没有订阅过 + int messageId = clientSession.getPacketId(); + MqttSubscribeMessage message = MqttSubscribeMessage.builder() + .addSubscriptions(topicSubscriptionList) + .messageId(messageId) + .properties(properties) + .build(); + // 4. 已经连接成功,直接订阅逻辑,未连接成功的添加到订阅列表,连接成功时会重连。 + ClientChannelContext clientContext = getContext(); + if (clientContext != null && clientContext.isAccepted()) { + MqttPendingSubscription pendingSubscription = new MqttPendingSubscription(needSubscriptionList, message); + pendingSubscription.startRetransmitTimer(taskService, clientContext); + clientSession.addPaddingSubscribe(messageId, pendingSubscription); + // gitee issues #IB72L6 先添加并启动重试,再发送订阅 + boolean result = Tio.send(clientContext, message); + logger.info("MQTT subscriptionList:{} messageId:{} subscribing result:{}", needSubscriptionList, messageId, result); + } else { + clientSession.addSubscriptionList(needSubscriptionList); + } + return this; + } + + /** + * 取消订阅 + * + * @param topicFilters topicFilter 集合 + * @return MqttClient + */ + public MqttClient unSubscribe(String... topicFilters) { + return unSubscribe(Arrays.asList(topicFilters)); + } + + /** + * 取消订阅 + * + * @param topicFilters topicFilter 集合 + * @return MqttClient + */ + public MqttClient unSubscribe(List topicFilters) { + // 1. 校验 topicFilter + TopicUtil.validateTopicFilter(topicFilters); + // 2. 优先取消本地订阅 + clientSession.removePaddingSubscribes(topicFilters); + clientSession.removeSubscriptions(topicFilters); + // 3. 发送取消订阅到服务端 + int messageId = clientSession.getPacketId(); + MqttUnSubscribeMessage message = MqttUnSubscribeMessage.builder() + .addTopicFilters(topicFilters) + .messageId(messageId) + .build(); + MqttPendingUnSubscription pendingUnSubscription = new MqttPendingUnSubscription(topicFilters, message); + ClientChannelContext clientContext = getContext(); + // 4. 启动取消订阅线程 + clientSession.addPaddingUnSubscribe(messageId, pendingUnSubscription); + pendingUnSubscription.startRetransmissionTimer(taskService, clientContext); + // 5. 发送取消订阅的消息 + boolean result = Tio.send(clientContext, message); + logger.info("MQTT Topic:{} messageId:{} unSubscribing result:{}", topicFilters, messageId, result); + return this; + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload) { + return publish(topic, payload, MqttQoS.QOS0); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos) { + return publish(topic, payload, qos, false); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, boolean retain) { + return publish(topic, payload, MqttQoS.QOS0, retain); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos, boolean retain) { + return publish(topic, payload, qos, (publishBuilder) -> publishBuilder.retained(retain)); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @param properties MqttProperties + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos, boolean retain, MqttProperties properties) { + return publish(topic, payload, qos, (publishBuilder) -> publishBuilder.retained(retain).properties(properties)); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param builder PublishBuilder + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos, Consumer builder) { + MqttPublishBuilder publishBuilder = MqttPublishMessage.builder(); + // 序列化 + byte[] newPayload = payload instanceof byte[] ? (byte[]) payload : mqttSerializer.serialize(payload); + // 自定义配置 + builder.accept(publishBuilder); + // 内置配置 + publishBuilder.topicName(topic) + .payload(newPayload) + .qos(qos); + return publish(publishBuilder); + } + + /** + * 发布消息 + * + * @param builder PublishBuilder + * @return 是否发送成功 + */ + public boolean publish(MqttPublishBuilder builder) { + String topic = Objects.requireNonNull(builder.getTopicName(), "topic is null"); + // 校验 topic + TopicUtil.validateTopicName(topic); + MqttQoS qos = Objects.requireNonNull(builder.getQos(), "qos is null"); + // qos 判断 + boolean isHighLevelQoS = MqttQoS.QOS1 == qos || MqttQoS.QOS2 == qos; + int messageId = isHighLevelQoS ? clientSession.getPacketId() : -1; + // 内置配置 + MqttPublishMessage message = builder + .messageId(messageId) + .build(); + ClientChannelContext clientContext = getContext(); + if (clientContext == null) { + logger.error("MQTT client publish fail, TCP not connected."); + return false; + } + // 如果已经连接成功,但是还没有 mqtt 认证,不进行休眠等待(避免大批量数据,卡死) + // https://gitee.com/dromara/mica-mqtt/issues/IC4DWT + if (!clientContext.isAccepted()) { + logger.error("TCP is connected but mqtt is not accepted."); + return false; + } + // 如果是高版本的 qos + if (isHighLevelQoS) { + MqttPendingPublish pendingPublish = new MqttPendingPublish(message, qos); + clientSession.addPendingPublish(messageId, pendingPublish); + pendingPublish.startPublishRetransmissionTimer(taskService, clientContext); + } + // 发送消息 + boolean result = Tio.send(clientContext, message); + logger.debug("MQTT Topic:{} qos:{} retain:{} publish result:{}", topic, qos, builder.isRetained(), result); + return result; + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay) { + return this.tioClient.schedule(command, delay); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay, Executor executor) { + return this.tioClient.schedule(command, delay, executor); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay) { + return this.tioClient.scheduleOnce(command, delay); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay, Executor executor) { + return this.tioClient.scheduleOnce(command, delay, executor); + } + + /** + * 异步连接 + * + * @return TioClient + */ + MqttClient start(boolean sync) { + // 启动 tio + Node node = new Node(config.getIp(), config.getPort()); + try { + if (sync) { + this.tioClient.connect(node, config.getBindIp(), 0, config.getTimeout()); + } else { + this.tioClient.asyncConnect(node, config.getBindIp(), 0, config.getTimeout()); + } + return this; + } catch (Exception e) { + throw new IllegalStateException("Mica mqtt client async start fail.", e); + } + } + + /** + * 重连 + */ + public void reconnect() { + ClientChannelContext channelContext = getContext(); + if (channelContext == null) { + return; + } + try { + // 判断是否 removed + if (channelContext.isRemoved()) { + channelContext.setRemoved(false); + } + tioClient.reconnect(channelContext, config.getTimeout()); + } catch (Exception e) { + logger.error("mqtt client reconnect error", e); + } + } + + /** + * 重连到新的服务端节点 + * + * @param ip ip + * @param port port + * @return 是否成功 + */ + public boolean reconnect(String ip, int port) { + return reconnect(new Node(ip, port)); + } + + /** + * 重连到新的服务端节点 + * + * @param serverNode Node + * @return 是否成功 + */ + public boolean reconnect(Node serverNode) { + // 更新 ip 和端口 + this.config.ip(serverNode.getIp()).port(serverNode.getPort()); + // 获取老的,老的有可能为 null,因为已经关闭,进入 closes 里:https://gitee.com/dromara/mica-mqtt/issues/IBY5LQ + ClientChannelContext oldContext = getContext(); + if (oldContext == null) { + // 如果是已经关闭的连接,设置 serverNode,下一次重连触发就会使用新的 serverNode + Set closedSet = clientTioConfig.closeds; + if (closedSet != null && !closedSet.isEmpty()) { + ChannelContext closedContext = closedSet.iterator().next(); + closedContext.setServerNode(serverNode); + } + } else { + // 切换 serverNode,关闭连接,触发重连任务去连接新的 serverNode + oldContext.setServerNode(serverNode); + Tio.close(oldContext, "切换服务地址:" + serverNode); + } + return false; + } + + /** + * 断开 mqtt 连接 + * + * @return 是否成功 + */ + public boolean disconnect() { + ClientChannelContext channelContext = getContext(); + if (channelContext == null) { + return false; + } + boolean result = Tio.bSend(channelContext, MqttMessage.DISCONNECT); + if (result) { + Tio.close(channelContext, null, "MqttClient disconnect.", true); + } + return result; + } + + /** + * 停止客户端 + * + * @return 是否停止成功 + */ + public boolean stop() { + // 1. 断开连接 + if (config.isDisconnectBeforeStop()) { + this.disconnect(); + } + // 2. 停止 tio + boolean result = tioClient.stop(); + // 3. 停止工作线程 + try { + mqttExecutor.shutdown(); + } catch (Exception e1) { + logger.error(e1.getMessage(), e1); + } + try { + // 等待线程池中的任务结束,客户端等待 6 秒基本上足够了 + result &= mqttExecutor.awaitTermination(6, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error(e.getMessage(), e); + } + logger.info("MqttClient stop result:{}", result); + // 4. 清理 session + this.clientSession.clean(); + return result; + } + + /** + * 获取 TioClient + * + * @return TioClient + */ + public TioClient getTioClient() { + return tioClient; + } + + /** + * 获取配置 + * + * @return MqttClientCreator + */ + public MqttClientCreator getClientCreator() { + return config; + } + + /** + * 获取 ClientTioConfig + * + * @return ClientTioConfig + */ + public TioClientConfig getClientTioConfig() { + return clientTioConfig; + } + + /** + * 获取 ClientChannelContext + * + * @return ClientChannelContext + */ + public ClientChannelContext getContext() { + if (context != null) { + return context; + } + synchronized (this) { + if (context == null) { + Set contextSet = Tio.getConnecteds(clientTioConfig); + if (contextSet != null && !contextSet.isEmpty()) { + this.context = (ClientChannelContext) contextSet.iterator().next(); + } + } + } + return this.context; + } + + /** + * 判断客户端跟服务端是否连接 + * + * @return 是否已经连接成功 + */ + public boolean isConnected() { + ClientChannelContext channelContext = getContext(); + return channelContext != null && channelContext.isAccepted(); + } + + /** + * 判断客户端跟服务端是否断开连接 + * + * @return 是否断连 + */ + public boolean isDisconnected() { + return !isConnected(); + } + + @Override + public MqttClient getMqttClient() { + return this; + } +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientAioHandler.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientAioHandler.java new file mode 100644 index 0000000..b8d718c --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientAioHandler.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.*; +import org.dromara.mica.mqtt.codec.message.*; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.tio.client.intf.TioClientHandler; +import org.tio.core.ChannelContext; +import org.tio.core.TioConfig; +import org.tio.core.exception.TioDecodeException; +import org.tio.core.intf.Packet; + +import java.nio.ByteBuffer; + +/** + * mqtt 客户端处理 + * + * @author L.cm + */ +public class MqttClientAioHandler implements TioClientHandler { + private final MqttDecoder mqttDecoder; + private final MqttEncoder mqttEncoder; + private final IMqttClientProcessor processor; + + public MqttClientAioHandler(MqttClientCreator mqttClientCreator, + IMqttClientProcessor processor) { + this.mqttDecoder = new MqttDecoder(mqttClientCreator.getMaxBytesInMessage(), mqttClientCreator.getMaxClientIdLength()); + this.mqttEncoder = MqttEncoder.INSTANCE; + this.processor = processor; + } + + @Override + public Packet heartbeatPacket(ChannelContext channelContext) { + return MqttMessage.PINGREQ; + } + + @Override + public Packet decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext context) throws TioDecodeException { + return mqttDecoder.doDecode(context, buffer, readableLength); + } + + @Override + public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext channelContext) { + return mqttEncoder.doEncode(channelContext, (MqttMessage) packet); + } + + @Override + public void handler(Packet packet, ChannelContext context) { + MqttMessage message = (MqttMessage) packet; + MqttFixedHeader fixedHeader = message.fixedHeader(); + // 根据消息类型处理消息 + MqttMessageType messageType = fixedHeader.messageType(); + switch (messageType) { + case CONNACK: + processor.processConAck(context, (MqttConnAckMessage) message); + break; + case SUBACK: + processor.processSubAck(context, (MqttSubAckMessage) message); + break; + case PUBLISH: + processor.processPublish(context, (MqttPublishMessage) message); + break; + case UNSUBACK: + processor.processUnSubAck((MqttUnSubAckMessage) message); + break; + case PUBACK: + processor.processPubAck((MqttPubAckMessage) message); + break; + case PUBREC: + processor.processPubRec(context, message); + break; + case PUBREL: + processor.processPubRel(context, message); + break; + case PUBCOMP: + processor.processPubComp(message); + break; + default: + break; + } + } + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientAioListener.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientAioListener.java new file mode 100644 index 0000000..ddede64 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientAioListener.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.MqttVersion; +import org.dromara.mica.mqtt.codec.message.MqttConnectMessage; +import org.dromara.mica.mqtt.codec.message.builder.MqttConnectBuilder; +import org.dromara.mica.mqtt.codec.properties.IntegerProperty; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.properties.MqttPropertyType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.client.DefaultTioClientListener; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; +import org.tio.utils.hutool.StrUtil; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; + +/** + * mqtt 客户端监听器 + * + * @author L.cm + */ +public class MqttClientAioListener extends DefaultTioClientListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientAioListener.class); + private final MqttClientCreator clientCreator; + private final IMqttClientConnectListener connectListener; + private final ExecutorService executor; + + public MqttClientAioListener(MqttClientCreator clientCreator) { + this.clientCreator = clientCreator; + this.connectListener = clientCreator.getConnectListener(); + this.executor = clientCreator.getMqttExecutor(); + } + + @Override + public void onAfterConnected(ChannelContext context, boolean isConnected, boolean isReconnect) { + if (isConnected) { + // 重连时,发送 mqtt 连接消息 + boolean result = Tio.bSend(context, getConnectMessage(this.clientCreator)); + logger.info("MqttClient reconnect send connect result:{}", result); + if (!result) { + // 如果重连未成功,直接关闭连接,等待后续重连 + Tio.close(context, "MqttClient reconnect send fail."); + } + } + } + + @Override + public void onBeforeClose(ChannelContext context, Throwable throwable, String remark, boolean isRemove) { + context.setAccepted(false); + // 先判断是否配置监听 + if (connectListener == null) { + return; + } + // 2. 触发客户断开连接事件 + executor.submit(() -> { + try { + connectListener.onDisconnect(context, throwable, remark, isRemove); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + }); + } + + /** + * 构造连接消息 + * + * @param mqttClientCreator MqttClientCreator + * @return MqttConnectMessage + */ + private static MqttConnectMessage getConnectMessage(MqttClientCreator mqttClientCreator) { + MqttWillMessage willMessage = mqttClientCreator.getWillMessage(); + MqttVersion version = mqttClientCreator.getVersion(); + int keepAliveSecs = mqttClientCreator.getKeepAliveSecs(); + // 1. 建立连接后发送 mqtt 连接的消息 + MqttConnectBuilder builder = MqttConnectMessage.builder() + .clientId(mqttClientCreator.getClientId()) + .username(mqttClientCreator.getUsername()) + .cleanStart(mqttClientCreator.isCleanStart()) + .protocolVersion(version) + // 心跳 + .keepAlive(keepAliveSecs > 0 ? keepAliveSecs : MqttClientCreator.DEFAULT_KEEP_ALIVE_SECS) + .willFlag(willMessage != null); + // 2. 密码 + String password = mqttClientCreator.getPassword(); + if (StrUtil.isNotBlank(password)) { + builder.password(password.getBytes(StandardCharsets.UTF_8)); + } + // 3. 遗嘱消息 + if (willMessage != null) { + builder.willTopic(willMessage.getTopic()) + .willMessage(willMessage.getMessage()) + .willRetain(willMessage.isRetain()) + .willQoS(willMessage.getQos()) + .willProperties(willMessage.getWillProperties()); + } + // 4. mqtt5 特性 + if (MqttVersion.MQTT_5 == version) { + MqttProperties properties = mqttClientCreator.getProperties(); + // Session Expiry Interval + Integer sessionExpiryInterval = mqttClientCreator.getSessionExpiryIntervalSecs(); + if (sessionExpiryInterval != null && sessionExpiryInterval > 0) { + if (properties == null) { + properties = new MqttProperties(); + } + properties.add(new IntegerProperty(MqttPropertyType.SESSION_EXPIRY_INTERVAL, sessionExpiryInterval)); + } + if (properties != null) { + builder.properties(properties); + } + } + return builder.build(); + } + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientConnectTestProcessor.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientConnectTestProcessor.java new file mode 100644 index 0000000..3fd74c0 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientConnectTestProcessor.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.codes.MqttConnectReasonCode; +import org.dromara.mica.mqtt.codec.message.*; +import org.dromara.mica.mqtt.codec.message.header.MqttConnAckVariableHeader; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; + +import java.util.concurrent.CompletableFuture; + +/** + * 默认的 mqtt 消息处理器 + * + * @author L.cm + */ +public class MqttClientConnectTestProcessor implements IMqttClientProcessor { + private final CompletableFuture future; + + public MqttClientConnectTestProcessor(CompletableFuture future) { + this.future = future; + } + + @Override + public void processConAck(ChannelContext context, MqttConnAckMessage message) { + MqttConnAckVariableHeader connAckVariableHeader = message.variableHeader(); + Tio.remove(context, "mqtt connect tested."); + future.complete(connAckVariableHeader.connectReturnCode()); + } + + @Override + public void processSubAck(ChannelContext context, MqttSubAckMessage message) { + + } + + @Override + public void processPublish(ChannelContext context, MqttPublishMessage message) { + + } + + @Override + public void processUnSubAck(MqttUnSubAckMessage message) { + + } + + @Override + public void processPubAck(MqttPubAckMessage message) { + + } + + @Override + public void processPubRec(ChannelContext context, MqttMessage message) { + + } + + @Override + public void processPubRel(ChannelContext context, MqttMessage message) { + + } + + @Override + public void processPubComp(MqttMessage message) { + + } + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientCreator.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientCreator.java new file mode 100644 index 0000000..cfb285a --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientCreator.java @@ -0,0 +1,802 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.*; +import org.dromara.mica.mqtt.codec.codes.MqttConnectReasonCode; +import org.dromara.mica.mqtt.codec.message.builder.MqttTopicSubscription; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.core.serializer.MqttJsonSerializer; +import org.dromara.mica.mqtt.core.serializer.MqttSerializer; +import org.tio.client.ReconnConf; +import org.tio.client.TioClient; +import org.tio.client.TioClientConfig; +import org.tio.client.intf.TioClientHandler; +import org.tio.client.intf.TioClientListener; +import org.tio.client.task.HeartbeatTimeoutStrategy; +import org.tio.core.Node; +import org.tio.core.TioConfig; +import org.tio.core.ssl.SslConfig; +import org.tio.core.task.HeartbeatMode; +import org.tio.utils.hutool.NetUtil; +import org.tio.utils.hutool.StrUtil; +import org.tio.utils.thread.ThreadUtils; +import org.tio.utils.thread.pool.SynThreadPoolExecutor; +import org.tio.utils.timer.DefaultTimerTaskService; +import org.tio.utils.timer.TimerTaskService; + +import java.io.InputStream; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * mqtt 客户端构造器 + * + * @author L.cm + * @author ChangJin Wei (魏昌进) + */ +public final class MqttClientCreator { + /** + * 默认的心跳超时 + */ + public static final int DEFAULT_KEEP_ALIVE_SECS = 60; + /** + * 名称 + */ + private String name = "Mica-Mqtt-Client"; + /** + * ip,可为空,默认为 127.0.0.1 + */ + private String ip = "127.0.0.1"; + /** + * 端口,默认:1883 + */ + private int port = 1883; + /** + * 超时时间,t-io 配置,可为 null,默认为:5秒 + */ + private Integer timeout; + /** + * 绑定 ip,绑定网卡,用于多网卡,默认为 null + */ + private String bindIp; + /** + * 接收数据的 buffer size,默认:8k + */ + private int readBufferSize = MqttConstant.DEFAULT_MAX_READ_BUFFER_SIZE; + /** + * 消息解析最大 bytes 长度,默认:8092 + */ + private int maxBytesInMessage = MqttConstant.DEFAULT_MAX_BYTES_IN_MESSAGE; + /** + * mqtt 3.1 会校验此参数为 23,为了减少问题设置成了 64 + */ + private int maxClientIdLength = MqttConstant.DEFAULT_MAX_CLIENT_ID_LENGTH; + /** + * Keep Alive (s),如果用户不希望框架层面做心跳相关工作,请把此值设为0或负数 + */ + private int keepAliveSecs = DEFAULT_KEEP_ALIVE_SECS; + /** + * 心跳检测模式,默认:最后请求时间 + */ + private HeartbeatMode heartbeatMode = HeartbeatMode.LAST_REQ; + /** + * 心跳超时策略,默认:发送 ping + */ + private HeartbeatTimeoutStrategy heartbeatTimeoutStrategy = HeartbeatTimeoutStrategy.PING; + /** + * SSL配置 + */ + private SslConfig sslConfig; + /** + * 自动重连 + */ + private boolean reconnect = true; + /** + * 重连的间隔时间,单位毫秒,默认:5000 + */ + private long reInterval = 5000; + /** + * 连续重连次数,当连续重连这么多次都失败时,不再重连。0和负数则一直重连 + */ + private int retryCount = 0; + /** + * 重连,重新订阅一个批次大小,默认:20 + */ + private int reSubscribeBatchSize = 20; + /** + * 客户端 id,默认:随机生成 + */ + private String clientId; + /** + * mqtt 协议,默认:MQTT_5 + */ + private MqttVersion version = MqttVersion.MQTT_5; + /** + * 用户名 + */ + private String username = null; + /** + * 密码 + */ + private String password = null; + /** + * 清除会话 + *

+ * false 表示如果订阅的客户机断线了,那么要保存其要推送的消息,如果其重新连接时,则将这些消息推送。 + * true 表示消除,表示客户机是第一次连接,消息所以以前的连接信息。 + *

+ */ + private boolean cleanStart = true; + /** + * mqtt 5.0 session 有效期,单位秒 + */ + private Integer sessionExpiryIntervalSecs; + /** + * 遗嘱消息 + */ + private MqttWillMessage willMessage; + /** + * mqtt5 properties + */ + private MqttProperties properties; + /** + * 连接监听器 + */ + private IMqttClientConnectListener connectListener; + /** + * 全局订阅 + */ + private Set globalSubscribe; + /** + * 全局消息监听器 + */ + private IMqttClientGlobalMessageListener globalMessageListener; + /** + * 客户端 session + */ + private IMqttClientSession clientSession; + /** + * 是否开启监控,默认:false 不开启,节省内存 + */ + private boolean statEnable = false; + /** + * debug + */ + private boolean debug = false; + /** + * tioExecutor + */ + private SynThreadPoolExecutor tioExecutor; + /** + * groupExecutor + */ + private ExecutorService groupExecutor; + /** + * mqttExecutor + */ + private ExecutorService mqttExecutor; + /** + * taskService + */ + private TimerTaskService taskService; + /** + * TioConfig 自定义配置 + */ + private Consumer tioConfigCustomize; + /** + * 序列化 + */ + private MqttSerializer mqttSerializer; + /** + * 停止前是否发送 disconnect 消息,默认:true 不会触发遗嘱消息 + */ + private boolean disconnectBeforeStop = true; + + public String getName() { + return name; + } + + public String getIp() { + return ip; + } + + public int getPort() { + return port; + } + + public Integer getTimeout() { + return timeout; + } + + public String getBindIp() { + return bindIp; + } + + public int getReadBufferSize() { + return readBufferSize; + } + + public int getMaxBytesInMessage() { + return maxBytesInMessage; + } + + public int getMaxClientIdLength() { + return maxClientIdLength; + } + + public int getKeepAliveSecs() { + return keepAliveSecs; + } + + public HeartbeatMode getHeartbeatMode() { + return heartbeatMode; + } + + public HeartbeatTimeoutStrategy getHeartbeatTimeoutStrategy() { + return heartbeatTimeoutStrategy; + } + + public SslConfig getSslConfig() { + return sslConfig; + } + + public boolean isReconnect() { + return reconnect; + } + + public int getRetryCount() { + return retryCount; + } + + public long getReInterval() { + return reInterval; + } + + public int getReSubscribeBatchSize() { + return reSubscribeBatchSize; + } + + public String getClientId() { + return clientId; + } + + public MqttVersion getVersion() { + return version; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public boolean isCleanStart() { + return cleanStart; + } + + public Integer getSessionExpiryIntervalSecs() { + return sessionExpiryIntervalSecs; + } + + public MqttWillMessage getWillMessage() { + return willMessage; + } + + public MqttProperties getProperties() { + return properties; + } + + public IMqttClientConnectListener getConnectListener() { + return connectListener; + } + + public Set getGlobalSubscribe() { + return globalSubscribe; + } + + public IMqttClientGlobalMessageListener getGlobalMessageListener() { + return globalMessageListener; + } + + public IMqttClientSession getClientSession() { + return clientSession; + } + + public boolean isStatEnable() { + return statEnable; + } + + public boolean isDebug() { + return debug; + } + + public SynThreadPoolExecutor getTioExecutor() { + return tioExecutor; + } + + public ExecutorService getGroupExecutor() { + return groupExecutor; + } + + public ExecutorService getMqttExecutor() { + return mqttExecutor; + } + + public TimerTaskService getTaskService() { + return taskService; + } + + public MqttSerializer getMqttSerializer() { + return mqttSerializer; + } + + public boolean isDisconnectBeforeStop() { + return disconnectBeforeStop; + } + + public MqttClientCreator name(String name) { + this.name = name; + return this; + } + + public MqttClientCreator ip(String ip) { + this.ip = ip; + return this; + } + + public MqttClientCreator port(int port) { + this.port = port; + return this; + } + + public MqttClientCreator timeout(int timeout) { + this.timeout = timeout; + return this; + } + + public MqttClientCreator bindIp(String bindIp) { + this.bindIp = bindIp; + return this; + } + + public MqttClientCreator bindNetworkInterface(String networkInterfaceName) { + if (StrUtil.isBlank(networkInterfaceName)) { + return this; + } else { + String ipV4 = NetUtil.getNetworkInterfaceIpV4(networkInterfaceName); + return bindIp(Objects.requireNonNull(ipV4, "获取网卡 ip 为 null")); + } + } + + public MqttClientCreator readBufferSize(int readBufferSize) { + this.readBufferSize = readBufferSize; + return this; + } + + public MqttClientCreator maxBytesInMessage(int maxBytesInMessage) { + this.maxBytesInMessage = maxBytesInMessage; + return this; + } + + public MqttClientCreator maxClientIdLength(int maxClientIdLength) { + this.maxClientIdLength = maxClientIdLength; + return this; + } + + public MqttClientCreator keepAliveSecs(int keepAliveSecs) { + this.keepAliveSecs = keepAliveSecs; + return this; + } + + public MqttClientCreator heartbeatMode(HeartbeatMode heartbeatMode) { + this.heartbeatMode = heartbeatMode; + return this; + } + + public MqttClientCreator heartbeatTimeoutStrategy(HeartbeatTimeoutStrategy heartbeatTimeoutStrategy) { + this.heartbeatTimeoutStrategy = heartbeatTimeoutStrategy; + return this; + } + + public MqttClientCreator useSsl() { + return sslConfig(SslConfig.forClient()); + } + + public MqttClientCreator useSsl(String trustStoreFile, String trustPassword) { + return sslConfig(SslConfig.forClient(trustStoreFile, trustPassword)); + } + + public MqttClientCreator useSsl(String keyStoreFile, String keyPasswd, String trustStoreFile, String trustPassword) { + return sslConfig(SslConfig.forClient(keyStoreFile, keyPasswd, trustStoreFile, trustPassword)); + } + + public MqttClientCreator useSsl(InputStream trustStoreInputStream, String trustPassword) { + return sslConfig(SslConfig.forClient(trustStoreInputStream, trustPassword)); + } + + public MqttClientCreator useSsl(InputStream keyStoreInputStream, String keyPasswd, InputStream trustStoreInputStream, String trustPassword) { + return sslConfig(SslConfig.forClient(keyStoreInputStream, keyPasswd, trustStoreInputStream, trustPassword)); + } + + public MqttClientCreator sslConfig(SslConfig sslConfig) { + this.sslConfig = sslConfig; + return this; + } + + public MqttClientCreator reconnect(boolean reconnect) { + this.reconnect = reconnect; + return this; + } + + public MqttClientCreator retryCount(int retryCount) { + this.retryCount = retryCount; + return this; + } + + public MqttClientCreator reInterval(long reInterval) { + this.reInterval = reInterval; + return this; + } + + public MqttClientCreator reSubscribeBatchSize(int reSubscribeBatchSize) { + this.reSubscribeBatchSize = reSubscribeBatchSize; + return this; + } + + public MqttClientCreator clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public MqttClientCreator version(MqttVersion version) { + this.version = version; + return this; + } + + public MqttClientCreator username(String username) { + this.username = username; + return this; + } + + public MqttClientCreator password(String password) { + this.password = password; + return this; + } + + public MqttClientCreator cleanStart(boolean cleanStart) { + this.cleanStart = cleanStart; + return this; + } + + public MqttClientCreator sessionExpiryIntervalSecs(Integer sessionExpiryIntervalSecs) { + this.sessionExpiryIntervalSecs = sessionExpiryIntervalSecs; + return this; + } + + public MqttClientCreator willMessage(MqttWillMessage willMessage) { + this.willMessage = willMessage; + return this; + } + + public MqttClientCreator willMessage(Consumer consumer) { + MqttWillMessage.Builder builder = MqttWillMessage.builder(); + consumer.accept(builder); + return willMessage(builder.build()); + } + + public MqttClientCreator properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttClientCreator connectListener(IMqttClientConnectListener connectListener) { + this.connectListener = connectListener; + return this; + } + + public MqttClientCreator globalSubscribe(String... topics) { + Objects.requireNonNull(topics, "globalSubscribe topics is null."); + List subscriptionList = Arrays.stream(topics) + .map(MqttTopicSubscription::new) + .collect(Collectors.toList()); + return globalSubscribe(subscriptionList); + } + + public MqttClientCreator globalSubscribe(MqttTopicSubscription... topics) { + Objects.requireNonNull(topics, "globalSubscribe topics is null."); + return globalSubscribe(Arrays.asList(topics)); + } + + public MqttClientCreator globalSubscribe(List topicList) { + Objects.requireNonNull(topicList, "globalSubscribe topicList is null."); + if (this.globalSubscribe == null) { + this.globalSubscribe = new HashSet<>(topicList); + } else { + this.globalSubscribe.addAll(topicList); + } + return this; + } + + public MqttClientCreator globalMessageListener(IMqttClientGlobalMessageListener globalMessageListener) { + this.globalMessageListener = globalMessageListener; + return this; + } + + public MqttClientCreator clientSession(IMqttClientSession clientSession) { + this.clientSession = clientSession; + return this; + } + + public MqttClientCreator statEnable() { + return statEnable(true); + } + + public MqttClientCreator statEnable(boolean enable) { + this.statEnable = enable; + return this; + } + + public MqttClientCreator debug() { + return debug(true); + } + + public MqttClientCreator debug(boolean debug) { + this.debug = debug; + return this; + } + + public MqttClientCreator tioExecutor(SynThreadPoolExecutor tioExecutor) { + this.tioExecutor = tioExecutor; + return this; + } + + public MqttClientCreator groupExecutor(ExecutorService groupExecutor) { + this.groupExecutor = groupExecutor; + return this; + } + + public MqttClientCreator mqttExecutor(ExecutorService mqttExecutor) { + this.mqttExecutor = mqttExecutor; + return this; + } + + public MqttClientCreator bizThreadPoolSize(int poolSize) { + if (poolSize <= 0) { + throw new IllegalArgumentException("poolSize must be greater than zero."); + } + return mqttExecutor(ThreadUtils.getBizExecutor(poolSize)); + } + + public MqttClientCreator taskService(TimerTaskService taskService) { + this.taskService = taskService; + return this; + } + + public MqttClientCreator tioConfigCustomize(Consumer tioConfigCustomize) { + this.tioConfigCustomize = tioConfigCustomize; + return this; + } + + public MqttClientCreator mqttJsonSerializer(MqttSerializer mqttSerializer) { + this.mqttSerializer = mqttSerializer; + return this; + } + + /** + * 停止前是否发送 disconnect 消息,默认:true 不会触发遗嘱消息 + */ + public MqttClientCreator disconnectBeforeStop() { + return disconnectBeforeStop(true); + } + + /** + * 停止前是否发送 disconnect 消息,默认:true 不会触发遗嘱消息 + */ + public MqttClientCreator disconnectBeforeStop(boolean disconnectBeforeStop) { + this.disconnectBeforeStop = disconnectBeforeStop; + return this; + } + + /** + * 构建一个新的 MqttClientCreator + * + * @return 新的 MqttClientCreator + */ + public MqttClientCreator newCreator() { + return new MqttClientCreator() + .name(this.name) + .ip(this.ip) + .port(this.port) + .timeout(this.timeout) + .bindIp(this.bindIp) + .readBufferSize(this.readBufferSize) + .maxBytesInMessage(this.maxBytesInMessage) + .maxClientIdLength(this.maxClientIdLength) + .keepAliveSecs(this.keepAliveSecs) + .sslConfig(this.sslConfig) + .reconnect(this.reconnect) + .reInterval(this.reInterval) + .retryCount(this.retryCount) + .reSubscribeBatchSize(this.reSubscribeBatchSize) + .version(this.version) + .cleanStart(this.cleanStart) + .sessionExpiryIntervalSecs(this.sessionExpiryIntervalSecs) + .willMessage(this.willMessage) + .connectListener(this.connectListener) + .statEnable(this.statEnable) + .debug(this.debug) + .mqttJsonSerializer(this.mqttSerializer) + .disconnectBeforeStop(this.disconnectBeforeStop); + } + + private MqttClient build() { + // 1. clientId 为空,生成默认的 clientId + if (StrUtil.isBlank(this.clientId)) { + // 默认为:MICA-MQTT- 前缀和 36进制的纳秒数 + this.clientId("MICA-MQTT-" + Long.toString(System.nanoTime(), 36)); + } + // 2. 客户端 session + if (this.clientSession == null) { + this.clientSession = new DefaultMqttClientSession(); + } + // tioExecutor + if (this.tioExecutor == null) { + this.tioExecutor = ThreadUtils.getTioExecutor(3); + } + // groupExecutor + if (this.groupExecutor == null) { + this.groupExecutor = ThreadUtils.getGroupExecutor(2); + } + // mqttExecutor + if (this.mqttExecutor == null) { + this.mqttExecutor = ThreadUtils.getBizExecutor(Math.max(2, ThreadUtils.CORE_POOL_SIZE)); + } + // taskService + if (this.taskService == null) { + this.taskService = new DefaultTimerTaskService(); + } + // heartbeatMode + if (this.heartbeatMode == null) { + this.heartbeatMode = HeartbeatMode.LAST_REQ; + } + if (this.mqttSerializer == null) { + this.mqttSerializer = new MqttJsonSerializer(); + } + IMqttClientProcessor processor = new DefaultMqttClientProcessor(this); + // 4. 初始化 mqtt 处理器 + TioClientHandler clientAioHandler = new MqttClientAioHandler(this, processor); + TioClientListener clientAioListener = new MqttClientAioListener(this); + // 5. 重连配置 + ReconnConf reconnConf = null; + if (this.reconnect) { + reconnConf = new ReconnConf(this.reInterval, this.retryCount); + } + // 6. tioConfig + TioClientConfig clientConfig = new TioClientConfig(clientAioHandler, clientAioListener, reconnConf, tioExecutor, groupExecutor); + clientConfig.setName(this.name); + // 7. 心跳超时时间 + clientConfig.setHeartbeatTimeout(TimeUnit.SECONDS.toMillis(this.keepAliveSecs)); + // 设置心跳检测模式为 LAST_REQ,keepAliveSecs 周期内,最后发送的时间差 + clientConfig.setHeartbeatMode(this.heartbeatMode); + clientConfig.setHeartbeatTimeoutStrategy(this.heartbeatTimeoutStrategy); + // 8. mqtt 消息最大长度,小于 1 则使用默认的,可通过 property tio.default.read.buffer.size 设置默认大小 + if (this.readBufferSize > 0) { + clientConfig.setReadBufferSize(this.readBufferSize); + } + // 9. ssl 证书设置 + if (this.sslConfig != null) { + clientConfig.setSslConfig(this.sslConfig); + // 内置 ssl 自定义配置,对 SNI 的支持 + if (this.sslConfig.getSslEngineCustomizer() == null) { + this.sslConfig.setSslEngineCustomizer(new MqttSSLEngineCustomizer(ip)); + } + } + // 10. 是否开启监控 + clientConfig.statOn = this.statEnable; + if (this.debug) { + clientConfig.debug = true; + } + // 11. 绑定 clientId 到 context 上,可以 context.getId() 获取 + clientConfig.setTioUuid(new MqttClientId(this)); + // 12. 自定义处理 + if (this.tioConfigCustomize != null) { + this.tioConfigCustomize.accept(clientConfig); + } + // 13. tioClient + try { + TioClient tioClient = new TioClient(clientConfig); + return new MqttClient(tioClient, this); + } catch (Exception e) { + throw new IllegalStateException("Mica mqtt client start fail.", e); + } + } + + /** + * 默认异步连接 + * + * @return TioClient + */ + public MqttClient connect() { + return this.build().start(false); + } + + /** + * 同步连接 + * + * @return TioClient + */ + public MqttClient connectSync() { + return this.build().start(true); + } + + /** + * 连接测试 + * + * @return MqttConnectReasonCode + */ + public MqttConnectReasonCode connectTest() { + return connectTest(3, TimeUnit.SECONDS); + } + + /** + * 连接测试 + * + * @param timeout timeout + * @param timeUnit TimeUnit + * @return MqttConnectReasonCode + */ + public MqttConnectReasonCode connectTest(long timeout, TimeUnit timeUnit) { + // 1. clientId 为空,生成默认的 clientId + if (StrUtil.isBlank(this.clientId)) { + // 默认为:MICA-MQTT- 前缀和 36进制的纳秒数 + this.clientId("MICA-MQTT-" + Long.toString(System.nanoTime(), 36)); + } + CompletableFuture future = new CompletableFuture<>(); + IMqttClientProcessor processor = new MqttClientConnectTestProcessor(future); + // 2. 初始化 mqtt 处理器 + TioClientHandler clientAioHandler = new MqttClientAioHandler(this, processor); + TioClientListener clientAioListener = new MqttClientAioListener(this); + // 3. tioConfig + TioClientConfig tioConfig = new TioClientConfig(clientAioHandler, clientAioListener); + tioConfig.setName(this.name); + // 4. 心跳超时时间,关闭心跳检测 + tioConfig.setHeartbeatTimeout(0); + TioClient tioClient; + try { + tioClient = new TioClient(tioConfig); + tioClient.asyncConnect(new Node(this.getIp(), this.getPort()), this.bindIp, 0, this.timeout); + } catch (Exception e) { + throw new IllegalStateException("Mica mqtt client start fail.", e); + } + try { + return future.get(timeout, timeUnit); + } catch (Exception e) { + // 超时,一般为服务器不可用 + return MqttConnectReasonCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE; + } finally { + tioClient.stop(); + } + } + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientCustomizer.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientCustomizer.java new file mode 100644 index 0000000..925a315 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientCustomizer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +/** + * MqttClient 配置自定义 + * + * @author L.cm + */ +@FunctionalInterface +public interface MqttClientCustomizer { + + /** + * MqttServerCreator 自定义扩展 + * + * @param creator MqttClientCreator + */ + void customize(MqttClientCreator creator); + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientId.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientId.java new file mode 100644 index 0000000..a37f177 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientId.java @@ -0,0 +1,21 @@ +package org.dromara.mica.mqtt.core.client; + +import org.tio.core.intf.TioUuid; + +/** + * 将 mqtt clientId 绑定到 context 中 + * + * @author L.cm + */ +public class MqttClientId implements TioUuid { + private final MqttClientCreator creator; + + public MqttClientId(MqttClientCreator creator) { + this.creator = creator; + } + + @Override + public String uuid() { + return creator.getClientId(); + } +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientSubscription.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientSubscription.java new file mode 100644 index 0000000..b6300b2 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientSubscription.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.builder.MqttTopicSubscription; +import org.dromara.mica.mqtt.core.common.TopicFilterType; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 发送订阅,未 ack 前的数据承载 + * + * @author L.cm + */ +public final class MqttClientSubscription implements Serializable { + private final String topicFilter; + private final MqttQoS mqttQoS; + private final TopicFilterType type; + private final transient IMqttClientMessageListener listener; + + public MqttClientSubscription(MqttQoS mqttQoS, + String topicFilter, + IMqttClientMessageListener listener) { + this.mqttQoS = Objects.requireNonNull(mqttQoS, "MQTT subscribe mqttQoS is null."); + this.topicFilter = Objects.requireNonNull(topicFilter, "MQTT subscribe topicFilter is null."); + this.type = TopicFilterType.getType(topicFilter); + this.listener = Objects.requireNonNull(listener, "MQTT subscribe listener is null."); + } + + public MqttQoS getMqttQoS() { + return mqttQoS; + } + + public String getTopicFilter() { + return topicFilter; + } + + public IMqttClientMessageListener getListener() { + return listener; + } + + public boolean matches(String topic) { + return this.type.match(this.topicFilter, topic); + } + + public MqttTopicSubscription toTopicSubscription() { + return new MqttTopicSubscription(topicFilter, mqttQoS); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MqttClientSubscription that = (MqttClientSubscription) o; + return Objects.equals(topicFilter, that.topicFilter) && + mqttQoS == that.mqttQoS && + Objects.equals(listener, that.listener); + } + + @Override + public int hashCode() { + return Objects.hash(topicFilter, mqttQoS, listener); + } + + @Override + public String toString() { + return "MqttClientSubscription{" + + "topicFilter='" + topicFilter + '\'' + + ", mqttQoS=" + mqttQoS + + '}'; + } + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttInvocationHandler.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttInvocationHandler.java new file mode 100644 index 0000000..5cf28ab --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttInvocationHandler.java @@ -0,0 +1,160 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.builder.MqttPublishBuilder; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.core.annotation.MqttClientPublish; +import org.dromara.mica.mqtt.core.annotation.MqttPayload; +import org.dromara.mica.mqtt.core.annotation.MqttRetain; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.tio.utils.hutool.CollUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; + +/** + * @author ChangJin Wei (魏昌进) + */ +public class MqttInvocationHandler implements InvocationHandler { + private final T mqttClient; + private final ConcurrentMap methodCache; + + public MqttInvocationHandler(T mqttClient) { + this.mqttClient = mqttClient; + this.methodCache = new ConcurrentHashMap<>(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 处理默认的 hashCode、equals 和 toString + if (Object.class.equals(method.getDeclaringClass())) { + return method.invoke(this, args); + } + // 其它代理方法 + MethodMetadata metadata = resolveMethod(method); + + Object payload = metadata.getPayloadIndex() >= 0 ? args[metadata.getPayloadIndex()] : null; + boolean retain = metadata.getRetainIndex() >= 0 && Boolean.TRUE.equals(args[metadata.getRetainIndex()]); + MqttProperties properties = metadata.getPropertiesIndex() >= 0 + ? (MqttProperties) args[metadata.getPropertiesIndex()] + : null; + Consumer builder = metadata.getBuilderIndex() >= 0 + ? (Consumer) args[metadata.getBuilderIndex()] + : null; + + String topic = TopicUtil.resolveTopic(metadata.getMqttPublish().value(), payload); + MqttQoS qos = metadata.getMqttPublish().qos(); + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException("Resolved topic is null or empty"); + } + MqttClient client = mqttClient.getMqttClient(); + if (builder == null) { + return client.publish(topic, payload, qos, retain, properties); + } else { + return client.publish(topic, payload, qos, builder); + } + } + + private MethodMetadata resolveMethod(Method method) { + return CollUtil.computeIfAbsent(methodCache, method, m -> { + MqttClientPublish mqttPublish = m.getAnnotation(MqttClientPublish.class); + if (mqttPublish == null) { + throw new UnsupportedOperationException("Method not annotated with @MqttClientPublish"); + } + + Annotation[][] paramAnnotations = m.getParameterAnnotations(); + Class[] paramTypes = m.getParameterTypes(); + + int payloadIndex = -1; + int retainIndex = -1; + int propertiesIndex = -1; + int builderIndex = -1; + + for (int i = 0; i < paramAnnotations.length; i++) { + for (Annotation annotation : paramAnnotations[i]) { + if (annotation instanceof MqttPayload) { + payloadIndex = i; + } else if (annotation instanceof MqttRetain) { + retainIndex = i; + } + } + } + + for (int i = 0; i < paramTypes.length; i++) { + if (propertiesIndex == -1 && MqttProperties.class.isAssignableFrom(paramTypes[i])) { + propertiesIndex = i; + } else if (builderIndex == -1 && Consumer.class.isAssignableFrom(paramTypes[i])) { + builderIndex = i; + } + } + + return new MethodMetadata(mqttPublish, payloadIndex, retainIndex, propertiesIndex, builderIndex); + }); + } + + private static class MethodMetadata { + + private final MqttClientPublish mqttPublish; + + private final int payloadIndex; + + private final int retainIndex; + + private final int propertiesIndex; + + private final int builderIndex; + + MethodMetadata(MqttClientPublish mqttPublish, + int payloadIndex, + int retainIndex, + int propertiesIndex, + int builderIndex) { + this.mqttPublish = mqttPublish; + this.payloadIndex = payloadIndex; + this.retainIndex = retainIndex; + this.propertiesIndex = propertiesIndex; + this.builderIndex = builderIndex; + } + + public MqttClientPublish getMqttPublish() { + return mqttPublish; + } + + public int getPayloadIndex() { + return payloadIndex; + } + + public int getRetainIndex() { + return retainIndex; + } + + public int getPropertiesIndex() { + return propertiesIndex; + } + + public int getBuilderIndex() { + return builderIndex; + } + } +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttPendingSubscription.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttPendingSubscription.java new file mode 100644 index 0000000..eb46cdf --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttPendingSubscription.java @@ -0,0 +1,62 @@ +package org.dromara.mica.mqtt.core.client; + + +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.codec.message.MqttSubscribeMessage; +import org.dromara.mica.mqtt.core.common.RetryProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; +import org.tio.utils.timer.TimerTaskService; + +import java.util.List; +import java.util.Objects; + +/** + * MqttPendingSubscription,参考于 netty-mqtt-client + */ +final class MqttPendingSubscription { + private static final Logger logger = LoggerFactory.getLogger(MqttPendingSubscription.class); + private final List subscriptionList; + private final RetryProcessor retryProcessor = new RetryProcessor<>(); + + MqttPendingSubscription(List subscriptionList, MqttSubscribeMessage message) { + this.subscriptionList = subscriptionList; + this.retryProcessor.setOriginalMessage(message); + } + + public List getSubscriptionList() { + return subscriptionList; + } + + void startRetransmitTimer(TimerTaskService taskService, ChannelContext context) { + this.retryProcessor.setHandle((fixedHeader, originalMessage) -> { + MqttMessage message = new MqttSubscribeMessage(fixedHeader, originalMessage.variableHeader(), originalMessage.payload()); + boolean result = Tio.send(context, message); + logger.info("retry send Subscribe topics:{} result:{}", subscriptionList, result); + }); + this.retryProcessor.start(taskService); + } + + void onSubAckReceived() { + this.retryProcessor.stop(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MqttPendingSubscription that = (MqttPendingSubscription) o; + return Objects.equals(subscriptionList, that.subscriptionList); + } + + @Override + public int hashCode() { + return Objects.hash(subscriptionList); + } +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttPendingUnSubscription.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttPendingUnSubscription.java new file mode 100644 index 0000000..69a3040 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttPendingUnSubscription.java @@ -0,0 +1,61 @@ +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.codec.message.MqttUnSubscribeMessage; +import org.dromara.mica.mqtt.core.common.RetryProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; +import org.tio.utils.timer.TimerTaskService; + +import java.util.List; +import java.util.Objects; + +/** + * MqttPendingSubscription,参考于 netty-mqtt-client + */ +final class MqttPendingUnSubscription { + private static final Logger logger = LoggerFactory.getLogger(MqttPendingUnSubscription.class); + private final List topics; + private final RetryProcessor retryProcessor = new RetryProcessor<>(); + + MqttPendingUnSubscription(List topics, MqttUnSubscribeMessage unSubscribeMessage) { + this.topics = topics; + this.retryProcessor.setOriginalMessage(unSubscribeMessage); + } + + List getTopics() { + return topics; + } + + void startRetransmissionTimer(TimerTaskService taskService, ChannelContext context) { + this.retryProcessor.setHandle((fixedHeader, originalMessage) -> { + MqttMessage message = new MqttUnSubscribeMessage(fixedHeader, originalMessage.variableHeader(), originalMessage.payload()); + boolean result = Tio.send(context, message); + logger.info("retry send Unsubscribe topics:{} result:{}", topics, result); + }); + this.retryProcessor.start(taskService); + } + + void onUnSubAckReceived() { + this.retryProcessor.stop(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MqttPendingUnSubscription that = (MqttPendingUnSubscription) o; + return Objects.equals(topics, that.topics); + } + + @Override + public int hashCode() { + return Objects.hash(topics); + } +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttSSLEngineCustomizer.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttSSLEngineCustomizer.java new file mode 100644 index 0000000..90ef140 --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttSSLEngineCustomizer.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.tio.core.ssl.SSLEngineCustomizer; + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import java.util.ArrayList; +import java.util.List; + +/** + * mqtt ssl 自定义配置 + * + * @author L.cm + */ +public class MqttSSLEngineCustomizer implements SSLEngineCustomizer { + /** + * ip 或域名 + */ + private final String host; + /** + * 端点识别算法,默认 null,生产环境建议配置成 HTTPS,支持:HTTPS/LDAPS/null + */ + private final String identificationAlgorithm; + + public MqttSSLEngineCustomizer(String host) { + this(host, null); + } + + public MqttSSLEngineCustomizer(String host, String identificationAlgorithm) { + this.host = host; + this.identificationAlgorithm = identificationAlgorithm; + } + + @Override + public void customize(SSLEngine engine) { + // SNI 支持 + SSLParameters sslParameters = engine.getSSLParameters(); + List sniHostNames = new ArrayList<>(1); + sniHostNames.add(new SNIHostName(host)); + sslParameters.setServerNames(sniHostNames); + // 端点识别算法 + if (identificationAlgorithm != null) { + sslParameters.setEndpointIdentificationAlgorithm(identificationAlgorithm); + } + engine.setSSLParameters(sslParameters); + } + +} diff --git a/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttWillMessage.java b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttWillMessage.java new file mode 100644 index 0000000..e105dab --- /dev/null +++ b/mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttWillMessage.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.client; + +import org.dromara.mica.mqtt.codec.message.properties.MqttWillPublishProperties; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.util.TopicUtil; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * 遗嘱消息 + * + * @author L.cm + */ +public final class MqttWillMessage { + private final String topic; + private final byte[] message; + /** + * 遗嘱消息保留标志 + */ + private final boolean retain; + /** + * 如果遗嘱标志被设置为 false,遗嘱 QoS 也必须设置为 0。 如果遗嘱标志被设置为 true,遗嘱 QoS 的值可以等于 0,1,2。 + */ + private final MqttQoS qos; + /** + * mqtt5 willProperties + */ + private final MqttProperties willProperties; + + private MqttWillMessage(String topic, byte[] message, boolean retain, MqttQoS qos, MqttProperties willProperties) { + this.topic = topic; + this.message = message; + this.retain = retain; + this.qos = qos; + this.willProperties = willProperties; + } + + public String getTopic() { + return topic; + } + + public byte[] getMessage() { + return message; + } + + public boolean isRetain() { + return retain; + } + + public MqttQoS getQos() { + return qos; + } + + public MqttProperties getWillProperties() { + return willProperties; + } + + public static MqttWillMessage.Builder builder() { + return new MqttWillMessage.Builder(); + } + + public static final class Builder { + private String topic; + private byte[] message; + /** + * 默认为不保存 + */ + private boolean retain = false; + /** + * 默认为 qos 0 + */ + private MqttQoS qos = MqttQoS.QOS0; + private MqttProperties willProperties; + + public Builder topic(String topic) { + TopicUtil.validateTopicName(topic); + this.topic = topic; + return this; + } + + public Builder message(byte[] message) { + this.message = Objects.requireNonNull(message); + return this; + } + + public Builder messageText(String message) { + this.message = Objects.requireNonNull(message).getBytes(StandardCharsets.UTF_8); + return this; + } + + public Builder retain(boolean retain) { + this.retain = retain; + return this; + } + + public Builder qos(MqttQoS qos) { + this.qos = Objects.requireNonNull(qos); + return this; + } + + public Builder willProperties(MqttProperties willProperties) { + this.willProperties = Objects.requireNonNull(willProperties); + return this; + } + + public Builder willProperties(Consumer consumer) { + MqttWillPublishProperties willPublishProperties = new MqttWillPublishProperties(); + consumer.accept(willPublishProperties); + return willProperties(willPublishProperties.getProperties()); + } + + public MqttWillMessage build() { +// 有效载荷中必须包含 Will Topic 和 Will Message字段 + Objects.requireNonNull(this.topic, "WillMessage topic is null."); + Objects.requireNonNull(this.message, "WillMessage message is null."); + return new MqttWillMessage(this.topic, this.message, this.retain, this.qos, this.willProperties); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MqttWillMessage that = (MqttWillMessage) o; + return retain == that.retain && + Objects.equals(topic, that.topic) && + Arrays.equals(message, that.message) && + qos == that.qos; + } + + @Override + public int hashCode() { + return Objects.hash(topic, Arrays.hashCode(message), retain, qos); + } + + @Override + public String toString() { + return "MqttWillMessage{" + + "topic='" + topic + '\'' + + ", message='" + Arrays.toString(message) + '\'' + + ", retain=" + retain + + ", qos=" + qos + + '}'; + } +} diff --git a/mica-mqtt-client/src/main/moditect/module-info.java b/mica-mqtt-client/src/main/moditect/module-info.java new file mode 100644 index 0000000..cb96528 --- /dev/null +++ b/mica-mqtt-client/src/main/moditect/module-info.java @@ -0,0 +1,4 @@ +open module org.dromara.mica.mqtt.client { + requires transitive org.dromara.mica.mqtt.common; + exports org.dromara.mica.mqtt.core.client; +} diff --git a/mica-mqtt-codec/pom.xml b/mica-mqtt-codec/pom.xml new file mode 100644 index 0000000..a34a8a8 --- /dev/null +++ b/mica-mqtt-codec/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + mica-mqtt + ${revision} + + mica-mqtt-codec + ${project.artifactId} + https://mica-mqtt.dreamlu.net + + + + net.dreamlu + mica-net-core + provided + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + org.moditect + moditect-maven-plugin + + + + + diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttCodecUtil.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttCodecUtil.java new file mode 100644 index 0000000..dba312a --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttCodecUtil.java @@ -0,0 +1,155 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec; + +import org.dromara.mica.mqtt.codec.exception.DecoderException; +import org.dromara.mica.mqtt.codec.exception.MqttUnacceptableProtocolVersionException; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +/** + * 编解码工具 + * + * @author netty + * @author L.cm + */ +public final class MqttCodecUtil { + private static final Logger logger = LoggerFactory.getLogger(MqttCodecUtil.class); + public static final char TOPIC_LAYER = '/'; + public static final char TOPIC_WILDCARDS_ONE = '+'; + public static final char TOPIC_WILDCARDS_MORE = '#'; + private static final String MQTT_VERSION_KEY = "MQTT_V"; + + private MqttCodecUtil() { + } + + /** + * mqtt 版本 + * + * @param ctx ChannelContext + * @return MqttVersion + */ + public static MqttVersion getMqttVersion(ChannelContext ctx) { + MqttVersion version = ctx.get(MQTT_VERSION_KEY); + if (version == null) { + return MqttVersion.MQTT_3_1_1; + } + return version; + } + + protected static void setMqttVersion(ChannelContext ctx, MqttVersion version) { + ctx.set(MQTT_VERSION_KEY, version); + } + + /** + * 判断是否 topic filter + * + * @param topicFilter topicFilter + * @return 是否 topic filter + */ + public static boolean isTopicFilter(String topicFilter) { + // 从尾部开始遍历,因为 + # 一般出现在 topicFilter 的尾部 + for (int i = topicFilter.length() - 1; i >= 0; i--) { + char ch = topicFilter.charAt(i); + // topic 中有空白符打印提示 + if (Character.isWhitespace(ch)) { + logger.warn("注意:topic:[{}] 中包含空白字符串:[{}],请检查是否正确", topicFilter, ch); + } else if (TOPIC_WILDCARDS_ONE == ch || TOPIC_WILDCARDS_MORE == ch) { + return true; + } + } + return false; + } + + protected static boolean isValidClientId(MqttVersion mqttVersion, int maxClientIdLength, String clientId) { + if (clientId == null) { + return false; + } + switch (mqttVersion) { + case MQTT_3_1: + return !clientId.isEmpty() && clientId.length() <= maxClientIdLength; + case MQTT_3_1_1: + case MQTT_5: + // In 3.1.3.1 Client Identifier of MQTT 3.1.1 and 5.0 specifications, The Server MAY allow ClientId’s + // that contain more than 23 encoded bytes. And, The Server MAY allow zero-length ClientId. + return true; + default: + throw new MqttUnacceptableProtocolVersionException(mqttVersion + " is unknown mqtt version"); + } + } + + protected static MqttFixedHeader validateFixedHeader(ChannelContext ctx, MqttFixedHeader mqttFixedHeader) { + switch (mqttFixedHeader.messageType()) { + case PUBREL: + case SUBSCRIBE: + case UNSUBSCRIBE: + if (MqttQoS.QOS1 != mqttFixedHeader.qosLevel()) { + throw new DecoderException(mqttFixedHeader.messageType().name() + " message must have QoS 1"); + } + return mqttFixedHeader; + case AUTH: + if (MqttVersion.MQTT_5 != MqttCodecUtil.getMqttVersion(ctx)) { + throw new DecoderException("AUTH message requires at least MQTT 5"); + } + return mqttFixedHeader; + default: + return mqttFixedHeader; + } + } + + protected static MqttFixedHeader resetUnusedFields(MqttFixedHeader mqttFixedHeader) { + switch (mqttFixedHeader.messageType()) { + case CONNECT: + case CONNACK: + case PUBACK: + case PUBREC: + case PUBCOMP: + case SUBACK: + case UNSUBACK: + case PINGREQ: + case PINGRESP: + case DISCONNECT: + if (mqttFixedHeader.isDup() || + MqttQoS.QOS0 != mqttFixedHeader.qosLevel() || + mqttFixedHeader.isRetain()) { + return new MqttFixedHeader( + mqttFixedHeader.messageType(), + false, + MqttQoS.QOS0, + false, + mqttFixedHeader.remainingLength()); + } + return mqttFixedHeader; + case PUBREL: + case SUBSCRIBE: + case UNSUBSCRIBE: + if (mqttFixedHeader.isRetain()) { + return new MqttFixedHeader( + mqttFixedHeader.messageType(), + mqttFixedHeader.isDup(), + mqttFixedHeader.qosLevel(), + false, + mqttFixedHeader.remainingLength()); + } + return mqttFixedHeader; + default: + return mqttFixedHeader; + } + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttConstant.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttConstant.java new file mode 100644 index 0000000..34e59d0 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttConstant.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec; + +/** + * mqtt 常量 + * + * @author netty + */ +public interface MqttConstant { + + /** + * mqtt protocol length + */ + int MQTT_PROTOCOL_LENGTH = 2; + + /** + * 默认 最大一次读取的 byte 字节数,默认:8k + */ + int DEFAULT_MAX_READ_BUFFER_SIZE = 8 * 1024; + + /** + * Default max bytes in message,默认:10M + */ + int DEFAULT_MAX_BYTES_IN_MESSAGE = 10 * 1024 * 1024; + + /** + * min client id length + */ + int MIN_CLIENT_ID_LENGTH = 1; + + /** + * Default max client id length,In the mqtt3.1 protocol, + * the default maximum Client Identifier length is 23,设置成 64,减少问题 + */ + int DEFAULT_MAX_CLIENT_ID_LENGTH = 64; + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttDecoder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttDecoder.java new file mode 100644 index 0000000..445c259 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttDecoder.java @@ -0,0 +1,727 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec; + +import org.dromara.mica.mqtt.codec.codes.MqttConnectReasonCode; +import org.dromara.mica.mqtt.codec.exception.DecoderException; +import org.dromara.mica.mqtt.codec.exception.MqttIdentifierRejectedException; +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.codec.message.builder.MqttSubscriptionOption; +import org.dromara.mica.mqtt.codec.message.builder.MqttTopicSubscription; +import org.dromara.mica.mqtt.codec.message.header.*; +import org.dromara.mica.mqtt.codec.message.payload.*; +import org.dromara.mica.mqtt.codec.properties.*; +import org.tio.core.ChannelContext; +import org.tio.core.intf.Packet; +import org.tio.utils.buffer.ByteBufferUtil; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Decodes Mqtt messages from bytes, following + * the MQTT protocol specification + * v3.1 + * or + * v5.0, depending on the + * version specified in the CONNECT message that first goes through the channel. + * + * @author netty + * @author L.cm + */ +public final class MqttDecoder { + private static final String MQTT_FIXED_HEADER_KEY = "MQTT_F_H_K"; + private final int maxBytesInMessage; + private final int maxClientIdLength; + + public MqttDecoder() { + this(MqttConstant.DEFAULT_MAX_BYTES_IN_MESSAGE); + } + + public MqttDecoder(int maxBytesInMessage) { + this(maxBytesInMessage, MqttConstant.DEFAULT_MAX_CLIENT_ID_LENGTH); + } + + public MqttDecoder(int maxBytesInMessage, int maxClientIdLength) { + this.maxBytesInMessage = maxBytesInMessage; + this.maxClientIdLength = maxClientIdLength; + } + + /** + * Decodes the fixed header. It's one byte for the flags and then variable bytes for the remaining length. + * + * @param buffer the buffer to decode from + * @return the fixed header + */ + private static MqttFixedHeader decodeFixedHeader(ChannelContext ctx, ByteBuffer buffer) { + short b1 = ByteBufferUtil.readUnsignedByte(buffer); + MqttMessageType messageType = MqttMessageType.valueOf(b1 >> 4); + boolean dupFlag = (b1 & 0x08) == 0x08; + int qosLevel = (b1 & 0x06) >> 1; + boolean retain = (b1 & 0x01) != 0; + int remainingLength = 0; + int multiplier = 1; + short digit; + int loops = 0; + do { + if (!buffer.hasRemaining()) { + return null; + } + digit = ByteBufferUtil.readUnsignedByte(buffer); + remainingLength += (digit & 127) * multiplier; + multiplier *= 128; + loops++; + } while ((digit & 128) != 0 && loops < 4); + // MQTT protocol limits Remaining Length to 4 bytes + if (loops == 4 && (digit & 128) != 0) { + throw new DecoderException("remaining length exceeds 4 digits (" + messageType + ')'); + } + int headLength = 1 + loops; + MqttFixedHeader decodedFixedHeader = new MqttFixedHeader(messageType, dupFlag, MqttQoS.valueOf(qosLevel), retain, headLength, remainingLength); + return MqttCodecUtil.validateFixedHeader(ctx, MqttCodecUtil.resetUnusedFields(decodedFixedHeader)); + } + + private static Result decodeMessageIdAndPropertiesVariableHeader( + ChannelContext ctx, ByteBuffer buffer, MqttFixedHeader mqttFixedHeader) { + final MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + final int packetId = decodeMessageId(buffer, mqttFixedHeader); + + final MqttMessageIdAndPropertiesVariableHeader mqttVariableHeader; + final int mqtt5Consumed; + + if (mqttVersion == MqttVersion.MQTT_5) { + final Result properties = decodeProperties(buffer); + mqttVariableHeader = new MqttMessageIdAndPropertiesVariableHeader(packetId, properties.value); + mqtt5Consumed = properties.numberOfBytesConsumed; + } else { + mqttVariableHeader = new MqttMessageIdAndPropertiesVariableHeader(packetId, MqttProperties.NO_PROPERTIES); + mqtt5Consumed = 0; + } + return new Result<>(mqttVariableHeader, 2 + mqtt5Consumed); + } + + /** + * decodeMessageId + * + * @param buffer ByteBuffer + * @param mqttFixedHeader MqttFixedHeader + * @return messageId with numberOfBytesConsumed is 2 + */ + private static int decodeMessageId(ByteBuffer buffer, MqttFixedHeader mqttFixedHeader) { + final int messageId = decodeMsbLsb(buffer); + // 注意:此处做 qos 降级处理,mqtt 规定 qos > 0,messageId 必须大于 0,固做降级处理 + if (messageId == 0) { + mqttFixedHeader.downgradeQos(); + } + return messageId; + } + + private static Result decodeSubAckPayload(ByteBuffer buffer, int bytesRemainingInVariablePart) { + final short[] grantedQos = new short[bytesRemainingInVariablePart]; + int numberOfBytesConsumed = 0; + while (numberOfBytesConsumed < bytesRemainingInVariablePart) { + short reasonCode = ByteBufferUtil.readUnsignedByte(buffer); + grantedQos[numberOfBytesConsumed] = reasonCode; + numberOfBytesConsumed++; + } + return new Result<>(new MqttSubAckPayload(grantedQos), numberOfBytesConsumed); + } + + private static Result decodeUnsubAckPayload(ByteBuffer buffer, int bytesRemainingInVariablePart) { + final short[] reasonCodes = new short[bytesRemainingInVariablePart]; + int numberOfBytesConsumed = 0; + while (numberOfBytesConsumed < bytesRemainingInVariablePart) { + short reasonCode = ByteBufferUtil.readUnsignedByte(buffer); + reasonCodes[numberOfBytesConsumed] = reasonCode; + numberOfBytesConsumed++; + } + return new Result<>(new MqttUnsubAckPayload(reasonCodes), numberOfBytesConsumed); + } + + private static Result decodeConnectionVariableHeader( + ChannelContext ctx, ByteBuffer buffer) { + final Result protoString = decodeString(buffer); + int numberOfBytesConsumed = protoString.numberOfBytesConsumed; + + final byte protocolLevel = buffer.get(); + numberOfBytesConsumed += 1; + + MqttVersion version = MqttVersion.fromProtocolNameAndLevel(protoString.value, protocolLevel); + MqttCodecUtil.setMqttVersion(ctx, version); + + final int b1 = ByteBufferUtil.readUnsignedByte(buffer); + numberOfBytesConsumed += 1; + + final int keepAlive = decodeMsbLsb(buffer); + numberOfBytesConsumed += 2; + + final boolean hasUserName = (b1 & 0x80) == 0x80; + final boolean hasPassword = (b1 & 0x40) == 0x40; + final boolean willRetain = (b1 & 0x20) == 0x20; + final int willQos = (b1 & 0x18) >> 3; + final boolean willFlag = (b1 & 0x04) == 0x04; + final boolean cleanStart = (b1 & 0x02) == 0x02; + if (version == MqttVersion.MQTT_3_1_1 || version == MqttVersion.MQTT_5) { + final boolean zeroReservedFlag = (b1 & 0x01) == 0x0; + if (!zeroReservedFlag) { + // MQTT v3.1.1: The Server MUST validate that the reserved flag in the CONNECT Control Packet is + // set to zero and disconnect the Client if it is not zero. + // See https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349230 + throw new DecoderException("non-zero reserved flag"); + } + } + + final MqttProperties properties; + if (version == MqttVersion.MQTT_5) { + final Result propertiesResult = decodeProperties(buffer); + properties = propertiesResult.value; + numberOfBytesConsumed += propertiesResult.numberOfBytesConsumed; + } else { + properties = MqttProperties.NO_PROPERTIES; + } + + final MqttConnectVariableHeader mqttConnectVariableHeader = new MqttConnectVariableHeader( + version.protocolName(), + version.protocolLevel(), + hasUserName, + hasPassword, + willRetain, + willQos, + willFlag, + cleanStart, + keepAlive, + properties); + return new Result<>(mqttConnectVariableHeader, numberOfBytesConsumed); + } + + private static Result decodeConnAckVariableHeader( + ChannelContext ctx, ByteBuffer buffer) { + final MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + final boolean sessionPresent = (ByteBufferUtil.readUnsignedByte(buffer) & 0x01) == 0x01; + byte returnCode = buffer.get(); + int numberOfBytesConsumed = 2; + + final MqttProperties properties; + if (mqttVersion == MqttVersion.MQTT_5) { + final Result propertiesResult = decodeProperties(buffer); + properties = propertiesResult.value; + numberOfBytesConsumed += propertiesResult.numberOfBytesConsumed; + } else { + properties = MqttProperties.NO_PROPERTIES; + } + + final MqttConnAckVariableHeader mqttConnAckVariableHeader = + new MqttConnAckVariableHeader(MqttConnectReasonCode.valueOf(returnCode), sessionPresent, properties); + return new Result<>(mqttConnAckVariableHeader, numberOfBytesConsumed); + } + + /** + * Decodes the payload. + * + * @param buffer the buffer to decode from + * @param messageType type of the message being decoded + * @param bytesRemainingInVariablePart bytes remaining + * @param variableHeader variable header of the same message + * @return the payload + */ + private static Result decodePayload(ByteBuffer buffer, int maxClientIdLength, + MqttMessageType messageType, int bytesRemainingInVariablePart, + Object variableHeader) { + switch (messageType) { + case CONNECT: + return decodeConnectionPayload(buffer, maxClientIdLength, (MqttConnectVariableHeader) variableHeader); + case SUBSCRIBE: + return decodeSubscribePayload(buffer, bytesRemainingInVariablePart); + case SUBACK: + return decodeSubAckPayload(buffer, bytesRemainingInVariablePart); + case UNSUBSCRIBE: + return decodeUnsubscribePayload(buffer, bytesRemainingInVariablePart); + case UNSUBACK: + return decodeUnsubAckPayload(buffer, bytesRemainingInVariablePart); + case PUBLISH: + return decodePublishPayload(buffer, bytesRemainingInVariablePart); + default: + // unknown payload , no byte consumed + return new Result<>(null, 0); + } + } + + private static Result decodeConnectionPayload(ByteBuffer buffer, int maxClientIdLength, + MqttConnectVariableHeader mqttConnectVariableHeader) { + final Result decodedClientId = decodeString(buffer); + final String decodedClientIdValue = decodedClientId.value; + final MqttVersion mqttVersion = MqttVersion.fromProtocolNameAndLevel(mqttConnectVariableHeader.name(), + (byte) mqttConnectVariableHeader.version()); + if (!MqttCodecUtil.isValidClientId(mqttVersion, maxClientIdLength, decodedClientIdValue)) { + throw new MqttIdentifierRejectedException("invalid clientIdentifier: " + decodedClientIdValue); + } + int numberOfBytesConsumed = decodedClientId.numberOfBytesConsumed; + + Result decodedWillTopic = null; + byte[] decodedWillMessage = null; + + final MqttProperties willProperties; + if (mqttConnectVariableHeader.isWillFlag()) { + if (mqttVersion == MqttVersion.MQTT_5) { + final Result propertiesResult = decodeProperties(buffer); + willProperties = propertiesResult.value; + numberOfBytesConsumed += propertiesResult.numberOfBytesConsumed; + } else { + willProperties = MqttProperties.NO_PROPERTIES; + } + decodedWillTopic = decodeString(buffer, 0, 32767); + numberOfBytesConsumed += decodedWillTopic.numberOfBytesConsumed; + decodedWillMessage = decodeByteArray(buffer); + numberOfBytesConsumed += decodedWillMessage.length + 2; + } else { + willProperties = MqttProperties.NO_PROPERTIES; + } + Result decodedUserName = null; + byte[] decodedPassword = null; + if (mqttConnectVariableHeader.hasUsername()) { + decodedUserName = decodeString(buffer); + numberOfBytesConsumed += decodedUserName.numberOfBytesConsumed; + } + if (mqttConnectVariableHeader.hasPassword()) { + decodedPassword = decodeByteArray(buffer); + numberOfBytesConsumed += decodedPassword.length + 2; + } + + final MqttConnectPayload mqttConnectPayload = + new MqttConnectPayload( + decodedClientId.value, + willProperties, + decodedWillTopic != null ? decodedWillTopic.value : null, + decodedWillMessage, + decodedUserName != null ? decodedUserName.value : null, + decodedPassword); + return new Result<>(mqttConnectPayload, numberOfBytesConsumed); + } + + private static Result decodeSubscribePayload(ByteBuffer buffer, int bytesRemainingInVariablePart) { + final List subscribeTopics = new ArrayList<>(); + int numberOfBytesConsumed = 0; + while (numberOfBytesConsumed < bytesRemainingInVariablePart) { + final Result decodedTopicName = decodeString(buffer); + numberOfBytesConsumed += decodedTopicName.numberOfBytesConsumed; + //See 3.8.3.1 Subscription Options of MQTT 5.0 specification for optionByte details + final short optionByte = ByteBufferUtil.readUnsignedByte(buffer); + + MqttQoS qos = MqttQoS.valueOf(optionByte & 0x03); + boolean noLocal = ((optionByte & 0x04) >> 2) == 1; + boolean retainAsPublished = ((optionByte & 0x08) >> 3) == 1; + MqttSubscriptionOption.RetainedHandlingPolicy retainHandling = + MqttSubscriptionOption.RetainedHandlingPolicy.valueOf((optionByte & 0x30) >> 4); + + final MqttSubscriptionOption subscriptionOption = new MqttSubscriptionOption(qos, + noLocal, retainAsPublished, retainHandling); + + numberOfBytesConsumed++; + subscribeTopics.add(new MqttTopicSubscription(decodedTopicName.value, subscriptionOption)); + } + return new Result<>(new MqttSubscribePayload(subscribeTopics), numberOfBytesConsumed); + } + + private static Result decodeUnsubscribePayload(ByteBuffer buffer, int bytesRemainingInVariablePart) { + final List unsubscribeTopics = new ArrayList<>(); + int numberOfBytesConsumed = 0; + while (numberOfBytesConsumed < bytesRemainingInVariablePart) { + final Result decodedTopicName = decodeString(buffer); + numberOfBytesConsumed += decodedTopicName.numberOfBytesConsumed; + unsubscribeTopics.add(decodedTopicName.value); + } + return new Result<>(new MqttUnsubscribePayload(unsubscribeTopics), numberOfBytesConsumed); + } + + private static Result decodePublishPayload(ByteBuffer buffer, int bytesRemainingInVariablePart) { + byte[] payload = new byte[bytesRemainingInVariablePart]; + buffer.get(payload, 0, bytesRemainingInVariablePart); + return new Result<>(payload, bytesRemainingInVariablePart); + } + + private static Result decodeString(ByteBuffer buffer) { + return decodeString(buffer, 0, Integer.MAX_VALUE); + } + + private static Result decodeString(ByteBuffer buffer, int minBytes, int maxBytes) { + int size = decodeMsbLsb(buffer); + int numberOfBytesConsumed = 2; + if (size < minBytes || size > maxBytes) { + ByteBufferUtil.skipBytes(buffer, size); + numberOfBytesConsumed += size; + return new Result<>(null, numberOfBytesConsumed); + } + String s = new String(buffer.array(), buffer.position(), size, StandardCharsets.UTF_8); + ByteBufferUtil.skipBytes(buffer, size); + numberOfBytesConsumed += size; + return new Result<>(s, numberOfBytesConsumed); + } + + /** + * @return the decoded byte[], numberOfBytesConsumed = byte[].length + 2 + */ + private static byte[] decodeByteArray(ByteBuffer buffer) { + int size = decodeMsbLsb(buffer); + byte[] bytes = new byte[size]; + buffer.get(bytes); + return bytes; + } + + // packing utils to reduce the amount of garbage while decoding ints + private static long packInts(int a, int b) { + return (((long) a) << 32) | (b & 0xFFFFFFFFL); + } + + private static int unpackA(long ints) { + return (int) (ints >> 32); + } + + private static int unpackB(long ints) { + return (int) ints; + } + + /** + * numberOfBytesConsumed = 2. return decoded result. + */ + private static int decodeMsbLsb(ByteBuffer buffer) { + int min = 0; + int max = 65535; + short msbSize = ByteBufferUtil.readUnsignedByte(buffer); + short lsbSize = ByteBufferUtil.readUnsignedByte(buffer); + int result = msbSize << 8 | lsbSize; + if (result < min || result > max) { + result = -1; + } + return result; + } + + /** + * See 1.5.5 Variable Byte Integer section of MQTT 5.0 specification for encoding/decoding rules + * + * @param buffer the buffer to decode from + * @return result pack with a = decoded integer, b = numberOfBytesConsumed. Need to unpack to read them. + * @throws DecoderException if bad MQTT protocol limits Remaining Length + */ + private static long decodeVariableByteInteger(ByteBuffer buffer) { + int remainingLength = 0; + int multiplier = 1; + short digit; + int loops = 0; + do { + digit = ByteBufferUtil.readUnsignedByte(buffer); + remainingLength += (digit & 127) * multiplier; + multiplier *= 128; + loops++; + } while ((digit & 128) != 0 && loops < 4); + + if (loops == 4 && (digit & 128) != 0) { + throw new DecoderException("MQTT protocol limits Remaining Length to 4 bytes"); + } + return packInts(remainingLength, loops); + } + + private static Result decodeProperties(ByteBuffer buffer) { + final long propertiesLength = decodeVariableByteInteger(buffer); + int totalPropertiesLength = unpackA(propertiesLength); + int numberOfBytesConsumed = unpackB(propertiesLength); + + MqttProperties decodedProperties = new MqttProperties(); + while (numberOfBytesConsumed < totalPropertiesLength) { + long propertyId = decodeVariableByteInteger(buffer); + final int propertyIdValue = unpackA(propertyId); + numberOfBytesConsumed += unpackB(propertyId); + MqttPropertyType propertyType = MqttPropertyType.valueOf(propertyIdValue); + switch (propertyType) { + case PAYLOAD_FORMAT_INDICATOR: + case REQUEST_PROBLEM_INFORMATION: + case REQUEST_RESPONSE_INFORMATION: + case MAXIMUM_QOS: + case RETAIN_AVAILABLE: + case WILDCARD_SUBSCRIPTION_AVAILABLE: + case SUBSCRIPTION_IDENTIFIER_AVAILABLE: + case SHARED_SUBSCRIPTION_AVAILABLE: + final int b1 = ByteBufferUtil.readUnsignedByte(buffer); + numberOfBytesConsumed++; + decodedProperties.add(new IntegerProperty(propertyIdValue, b1)); + break; + case SERVER_KEEP_ALIVE: + case RECEIVE_MAXIMUM: + case TOPIC_ALIAS_MAXIMUM: + case TOPIC_ALIAS: + final int int2BytesResult = decodeMsbLsb(buffer); + numberOfBytesConsumed += 2; + decodedProperties.add(new IntegerProperty(propertyIdValue, int2BytesResult)); + break; + case MESSAGE_EXPIRY_INTERVAL: + case SESSION_EXPIRY_INTERVAL: + case WILL_DELAY_INTERVAL: + case MAXIMUM_PACKET_SIZE: + final int maxPacketSize = buffer.getInt(); + numberOfBytesConsumed += 4; + decodedProperties.add(new IntegerProperty(propertyIdValue, maxPacketSize)); + break; + case SUBSCRIPTION_IDENTIFIER: + long vbIntegerResult = decodeVariableByteInteger(buffer); + numberOfBytesConsumed += unpackB(vbIntegerResult); + decodedProperties.add(new IntegerProperty(propertyIdValue, unpackA(vbIntegerResult))); + break; + case CONTENT_TYPE: + case RESPONSE_TOPIC: + case ASSIGNED_CLIENT_IDENTIFIER: + case AUTHENTICATION_METHOD: + case RESPONSE_INFORMATION: + case SERVER_REFERENCE: + case REASON_STRING: + final Result stringResult = decodeString(buffer); + numberOfBytesConsumed += stringResult.numberOfBytesConsumed; + decodedProperties.add(new StringProperty(propertyIdValue, stringResult.value)); + break; + case USER_PROPERTY: + final Result keyResult = decodeString(buffer); + final Result valueResult = decodeString(buffer); + numberOfBytesConsumed += keyResult.numberOfBytesConsumed; + numberOfBytesConsumed += valueResult.numberOfBytesConsumed; + decodedProperties.add(new UserProperty(keyResult.value, valueResult.value)); + break; + case CORRELATION_DATA: + case AUTHENTICATION_DATA: + final byte[] binaryDataResult = decodeByteArray(buffer); + numberOfBytesConsumed += binaryDataResult.length + 2; + decodedProperties.add(new BinaryProperty(propertyIdValue, binaryDataResult)); + break; + default: + //shouldn't reach here + throw new DecoderException("Unknown property type: " + propertyType); + } + } + return new Result<>(decodedProperties, numberOfBytesConsumed); + } + + public Packet doDecode(ChannelContext ctx, ByteBuffer buffer, int readableLength) { + // 1. 解析消息头 + MqttFixedHeader mqttFixedHeader = getOrDecodeMqttFixedHeader(ctx, buffer, readableLength); + if (mqttFixedHeader == null) { + return null; + } + // 2. 判断消息长度 + int messageLength = mqttFixedHeader.getMessageLength(); + if (readableLength < messageLength) { + return null; + } + // 清除缓存 + ctx.remove(MQTT_FIXED_HEADER_KEY); + // 3. 判断是否 ping 消息,ping 只有消息类型 + MqttMessageType messageType = mqttFixedHeader.messageType(); + if (MqttMessageType.PINGREQ == messageType) { + return MqttMessage.PINGREQ; + } else if (MqttMessageType.PINGRESP == messageType) { + return MqttMessage.PINGRESP; + } + return decodeMqttMessage(ctx, buffer, messageType, mqttFixedHeader); + } + + private Result decodePubReplyMessage( + ByteBuffer buffer, MqttFixedHeader mqttFixedHeader, int bytesRemainingInVariablePart) { + final int packetId = decodeMessageId(buffer, mqttFixedHeader); + final MqttPubReplyMessageVariableHeader mqttPubAckVariableHeader; + final int consumed; + final int packetIdNumberOfBytesConsumed = 2; + if (bytesRemainingInVariablePart > 3) { + final byte reasonCode = buffer.get(); + final Result properties = decodeProperties(buffer); + mqttPubAckVariableHeader = new MqttPubReplyMessageVariableHeader(packetId, + reasonCode, + properties.value); + consumed = packetIdNumberOfBytesConsumed + 1 + properties.numberOfBytesConsumed; + } else if (bytesRemainingInVariablePart > 2) { + final byte reasonCode = buffer.get(); + mqttPubAckVariableHeader = new MqttPubReplyMessageVariableHeader(packetId, + reasonCode, + MqttProperties.NO_PROPERTIES); + consumed = packetIdNumberOfBytesConsumed + 1; + } else { + mqttPubAckVariableHeader = new MqttPubReplyMessageVariableHeader(packetId, + (byte) 0, + MqttProperties.NO_PROPERTIES); + consumed = packetIdNumberOfBytesConsumed; + } + + return new Result<>(mqttPubAckVariableHeader, consumed); + } + + private Result decodeReasonCodeAndPropertiesVariableHeader( + ByteBuffer buffer, int bytesRemainingInVariablePart) { + final byte reasonCode; + final MqttProperties properties; + final int consumed; + if (bytesRemainingInVariablePart > 1) { + reasonCode = buffer.get(); + final Result propertiesResult = decodeProperties(buffer); + properties = propertiesResult.value; + consumed = 1 + propertiesResult.numberOfBytesConsumed; + } else if (bytesRemainingInVariablePart > 0) { + reasonCode = buffer.get(); + properties = MqttProperties.NO_PROPERTIES; + consumed = 1; + } else { + reasonCode = 0; + properties = MqttProperties.NO_PROPERTIES; + consumed = 0; + } + final MqttReasonCodeAndPropertiesVariableHeader mqttReasonAndPropsVariableHeader = + new MqttReasonCodeAndPropertiesVariableHeader(reasonCode, properties); + return new Result<>(mqttReasonAndPropsVariableHeader, consumed); + } + + private MqttFixedHeader getOrDecodeMqttFixedHeader(ChannelContext ctx, ByteBuffer buffer, int readableLength) { + // 1. 缓存避免重复解析 + MqttFixedHeader mqttFixedHeader = ctx.get(MQTT_FIXED_HEADER_KEY); + if (mqttFixedHeader != null) { + ByteBufferUtil.skipBytes(buffer, mqttFixedHeader.headLength()); + return mqttFixedHeader; + } + // 2. 判断缓存中协议头是否读完(MQTT协议头为2字节) + if (readableLength < MqttConstant.MQTT_PROTOCOL_LENGTH) { + return null; + } + // 3. 解析 FixedHeader 2~5 个字节 + buffer.mark(); + mqttFixedHeader = decodeFixedHeader(ctx, buffer); + // 不够读 + if (mqttFixedHeader == null) { + buffer.reset(); + return null; + } + int messageLength = mqttFixedHeader.getMessageLength(); + // 超过最大包 + if (messageLength > maxBytesInMessage) { + throw new DecoderException("too large message: " + messageLength + " bytes but maxBytesInMessage is " + maxBytesInMessage); + } + // 存储固定头,避免重复解析 + ctx.set(MQTT_FIXED_HEADER_KEY, mqttFixedHeader); + // 3. 长度不够,直接返回 null + if (readableLength < messageLength) { + ctx.setPacketNeededLength(messageLength); + return null; + } + return mqttFixedHeader; + } + + private MqttMessage decodeMqttMessage(ChannelContext ctx, ByteBuffer buffer, MqttMessageType messageType, + MqttFixedHeader mqttFixedHeader) { + // 1. 消息体长度 + int bytesRemainingInVariablePart = mqttFixedHeader.remainingLength(); + // 2. 解析头信息 + Object variableHeader; + try { + Result decodedVariableHeader = decodeVariableHeader(ctx, buffer, messageType, mqttFixedHeader, bytesRemainingInVariablePart); + variableHeader = decodedVariableHeader.value; + bytesRemainingInVariablePart -= decodedVariableHeader.numberOfBytesConsumed; + } catch (Exception cause) { + throw new DecoderException(cause); + } + // 3. 解析消息体 + final Result decodedPayload; + decodedPayload = decodePayload(buffer, maxClientIdLength, messageType, bytesRemainingInVariablePart, variableHeader); + bytesRemainingInVariablePart -= decodedPayload.numberOfBytesConsumed; + if (bytesRemainingInVariablePart != 0) { + throw new DecoderException("non-zero remaining payload bytes: " + bytesRemainingInVariablePart + " (" + mqttFixedHeader.messageType() + ')'); + } + return MqttMessageFactory.newMessage(mqttFixedHeader, variableHeader, decodedPayload.value); + } + + /** + * Decodes the variable header (if any) + * + * @param buffer the buffer to decode from + * @param mqttFixedHeader MqttFixedHeader of the same message + * @return the variable header + */ + private Result decodeVariableHeader(ChannelContext ctx, ByteBuffer buffer, + MqttMessageType messageType, + MqttFixedHeader mqttFixedHeader, + int bytesRemainingInVariablePart) { + switch (messageType) { + case CONNECT: + return decodeConnectionVariableHeader(ctx, buffer); + case CONNACK: + return decodeConnAckVariableHeader(ctx, buffer); + case UNSUBSCRIBE: + case SUBSCRIBE: + case SUBACK: + case UNSUBACK: + return decodeMessageIdAndPropertiesVariableHeader(ctx, buffer, mqttFixedHeader); + case PUBACK: + case PUBREC: + case PUBCOMP: + case PUBREL: + return decodePubReplyMessage(buffer, mqttFixedHeader, bytesRemainingInVariablePart); + case PUBLISH: + return decodePublishVariableHeader(ctx, buffer, mqttFixedHeader); + case DISCONNECT: + case AUTH: + return decodeReasonCodeAndPropertiesVariableHeader(buffer, bytesRemainingInVariablePart); + default: + //shouldn't reach here + throw new DecoderException("Unknown message type: " + messageType); + } + } + + private Result decodePublishVariableHeader( + ChannelContext ctx, ByteBuffer buffer, + MqttFixedHeader mqttFixedHeader) { + final MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + final Result decodedTopic = decodeString(buffer); + // 校验发布的 topic name,不能包含通配符 + if (MqttCodecUtil.isTopicFilter(decodedTopic.value)) { + throw new DecoderException("invalid publish topic name: " + decodedTopic.value + " (contains wildcards)"); + } + int numberOfBytesConsumed = decodedTopic.numberOfBytesConsumed; + + int messageId = -1; + if (mqttFixedHeader.qosLevel().value() > 0) { + messageId = decodeMessageId(buffer, mqttFixedHeader); + numberOfBytesConsumed += 2; + } + + final MqttProperties properties; + if (mqttVersion == MqttVersion.MQTT_5) { + final Result propertiesResult = decodeProperties(buffer); + properties = propertiesResult.value; + numberOfBytesConsumed += propertiesResult.numberOfBytesConsumed; + } else { + properties = MqttProperties.NO_PROPERTIES; + } + + final MqttPublishVariableHeader mqttPublishVariableHeader = + new MqttPublishVariableHeader(decodedTopic.value, messageId, properties); + return new Result<>(mqttPublishVariableHeader, numberOfBytesConsumed); + } + + private static final class Result { + + private final T value; + private final int numberOfBytesConsumed; + + Result(T value, int numberOfBytesConsumed) { + this.value = value; + this.numberOfBytesConsumed = numberOfBytesConsumed; + } + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttEncoder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttEncoder.java new file mode 100644 index 0000000..0f0540e --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttEncoder.java @@ -0,0 +1,618 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec; + +import org.dromara.mica.mqtt.codec.exception.EncoderException; +import org.dromara.mica.mqtt.codec.exception.MqttIdentifierRejectedException; +import org.dromara.mica.mqtt.codec.message.*; +import org.dromara.mica.mqtt.codec.message.builder.MqttSubscriptionOption; +import org.dromara.mica.mqtt.codec.message.builder.MqttTopicSubscription; +import org.dromara.mica.mqtt.codec.message.header.*; +import org.dromara.mica.mqtt.codec.message.payload.MqttConnectPayload; +import org.dromara.mica.mqtt.codec.message.payload.MqttSubscribePayload; +import org.dromara.mica.mqtt.codec.message.payload.MqttUnsubAckPayload; +import org.dromara.mica.mqtt.codec.message.payload.MqttUnsubscribePayload; +import org.dromara.mica.mqtt.codec.properties.*; +import org.tio.core.ChannelContext; +import org.tio.utils.buffer.ByteBufferUtil; +import org.tio.utils.hutool.FastByteBuffer; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * Encodes Mqtt messages into bytes following the protocol specification v3.1 + * as described here MQTTV3.1 + * or v5.0 as described here MQTTv5.0 - + * depending on the version specified in the first CONNECT message that goes through the channel. + * + * @author netty + * @author L.cm + */ +public final class MqttEncoder { + public static final MqttEncoder INSTANCE = new MqttEncoder(); + + private MqttEncoder() { + } + + private static ByteBuffer encodeConnectMessage(ChannelContext ctx, MqttConnectMessage message) { + int payloadBufferSize = 0; + + MqttFixedHeader mqttFixedHeader = message.fixedHeader(); + MqttConnectVariableHeader variableHeader = message.variableHeader(); + MqttConnectPayload payload = message.payload(); + MqttVersion mqttVersion = MqttVersion.fromProtocolNameAndLevel(variableHeader.name(), (byte) variableHeader.version()); + MqttCodecUtil.setMqttVersion(ctx, mqttVersion); + + // as MQTT 3.1 & 3.1.1 spec, If the User Name Flag is set to 0, the Password Flag MUST be set to 0 + if (!variableHeader.hasUsername() && variableHeader.hasPassword()) { + throw new EncoderException("Without a username, the password MUST be not set"); + } + + // Client id + String clientIdentifier = payload.clientIdentifier(); + if (!MqttCodecUtil.isValidClientId(mqttVersion, MqttConstant.DEFAULT_MAX_CLIENT_ID_LENGTH, clientIdentifier)) { + throw new MqttIdentifierRejectedException("invalid clientIdentifier: " + clientIdentifier); + } + byte[] clientIdentifierBytes = encodeStringUtf8(clientIdentifier); + payloadBufferSize += 2 + clientIdentifierBytes.length; + + // Will topic and message + String willTopic = payload.willTopic(); + byte[] willTopicBytes = willTopic != null ? encodeStringUtf8(willTopic) : ByteBufferUtil.EMPTY_BYTES; + byte[] willMessage = payload.willMessageInBytes(); + byte[] willMessageBytes = willMessage != null ? willMessage : ByteBufferUtil.EMPTY_BYTES; + if (variableHeader.isWillFlag()) { + payloadBufferSize += 2 + willTopicBytes.length; + payloadBufferSize += 2 + willMessageBytes.length; + } + + String userName = payload.username(); + byte[] userNameBytes = userName != null ? encodeStringUtf8(userName) : ByteBufferUtil.EMPTY_BYTES; + if (variableHeader.hasUsername()) { + payloadBufferSize += 2 + userNameBytes.length; + } + + byte[] password = payload.passwordInBytes(); + byte[] passwordBytes = password != null ? password : ByteBufferUtil.EMPTY_BYTES; + if (variableHeader.hasPassword()) { + payloadBufferSize += 2 + passwordBytes.length; + } + + // Fixed and variable header + byte[] protocolNameBytes = mqttVersion.protocolNameBytes(); + byte[] propertiesBytes = encodePropertiesIfNeeded(mqttVersion, message.variableHeader().properties()); + + final byte[] willPropertiesBytes; + if (variableHeader.isWillFlag()) { + willPropertiesBytes = encodePropertiesIfNeeded(mqttVersion, payload.willProperties()); + payloadBufferSize += willPropertiesBytes.length; + } else { + willPropertiesBytes = ByteBufferUtil.EMPTY_BYTES; + } + int variableHeaderBufferSize = 2 + protocolNameBytes.length + 4 + propertiesBytes.length; + + int variablePartSize = variableHeaderBufferSize + payloadBufferSize; + int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize); + // 申请 ByteBuffer + ByteBuffer buf = ByteBuffer.allocate(fixedHeaderBufferSize + variablePartSize); + buf.put(getFixedHeaderByte1(mqttFixedHeader)); + writeVariableLengthInt(buf, variablePartSize); + buf.putShort((short) protocolNameBytes.length); + buf.put(protocolNameBytes); + + buf.put((byte) variableHeader.version()); + buf.put((byte) getConnVariableHeaderFlag(variableHeader)); + buf.putShort((short) variableHeader.keepAliveTimeSeconds()); + buf.put(propertiesBytes); + + // Payload + buf.putShort((short) clientIdentifierBytes.length); + buf.put(clientIdentifierBytes, 0, clientIdentifierBytes.length); + if (variableHeader.isWillFlag()) { + buf.put(willPropertiesBytes, 0, willPropertiesBytes.length); + buf.putShort((short) willTopicBytes.length); + buf.put(willTopicBytes, 0, willTopicBytes.length); + buf.putShort((short) willMessageBytes.length); + buf.put(willMessageBytes, 0, willMessageBytes.length); + } + if (variableHeader.hasUsername()) { + buf.putShort((short) userNameBytes.length); + buf.put(userNameBytes, 0, userNameBytes.length); + } + if (variableHeader.hasPassword()) { + buf.putShort((short) passwordBytes.length); + buf.put(passwordBytes, 0, passwordBytes.length); + } + return buf; + } + + private static int getConnVariableHeaderFlag(MqttConnectVariableHeader variableHeader) { + int flagByte = 0; + if (variableHeader.hasUsername()) { + flagByte |= 0x80; + } + if (variableHeader.hasPassword()) { + flagByte |= 0x40; + } + if (variableHeader.isWillRetain()) { + flagByte |= 0x20; + } + flagByte |= (variableHeader.willQos() & 0x03) << 3; + if (variableHeader.isWillFlag()) { + flagByte |= 0x04; + } + if (variableHeader.isCleanStart()) { + flagByte |= 0x02; + } + return flagByte; + } + + private static ByteBuffer encodeConnAckMessage(ChannelContext ctx, MqttConnAckMessage message) { + final MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + byte[] propertiesBytes = encodePropertiesIfNeeded(mqttVersion, message.variableHeader().properties()); + ByteBuffer buf = ByteBuffer.allocate(4 + propertiesBytes.length); + buf.put(getFixedHeaderByte1(message.fixedHeader())); + writeVariableLengthInt(buf, 2 + propertiesBytes.length); + buf.put((byte) (message.variableHeader().isSessionPresent() ? 0x01 : 0x00)); + buf.put(message.variableHeader().connectReturnCode().value()); + buf.put(propertiesBytes); + return buf; + } + + private static ByteBuffer encodeSubscribeMessage(ChannelContext ctx, MqttSubscribeMessage message) { + MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + byte[] propertiesBytes = encodePropertiesIfNeeded(mqttVersion, + message.idAndPropertiesVariableHeader().properties()); + + final int variableHeaderBufferSize = 2 + propertiesBytes.length; + int payloadBufferSize = 0; + + MqttFixedHeader mqttFixedHeader = message.fixedHeader(); + MqttMessageIdVariableHeader variableHeader = message.variableHeader(); + MqttSubscribePayload payload = message.payload(); + + for (MqttTopicSubscription topic : payload.topicSubscriptions()) { + String topicFilter = topic.topicFilter(); + byte[] topicFilterBytes = encodeStringUtf8(topicFilter); + payloadBufferSize += 2 + topicFilterBytes.length; + payloadBufferSize += 1; + } + + int variablePartSize = variableHeaderBufferSize + payloadBufferSize; + int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize); + + ByteBuffer buf = ByteBuffer.allocate(fixedHeaderBufferSize + variablePartSize); + buf.put(getFixedHeaderByte1(mqttFixedHeader)); + writeVariableLengthInt(buf, variablePartSize); + + // Variable Header + int messageId = variableHeader.messageId(); + buf.putShort((short) messageId); + buf.put(propertiesBytes); + + // Payload + for (MqttTopicSubscription topic : payload.topicSubscriptions()) { + // topicName + String topicName = topic.topicFilter(); + byte[] topicNameBytes = encodeStringUtf8(topicName); + buf.putShort((short) topicNameBytes.length); + buf.put(topicNameBytes, 0, topicNameBytes.length); + if (mqttVersion == MqttVersion.MQTT_3_1_1 || mqttVersion == MqttVersion.MQTT_3_1) { + buf.put((byte) topic.qualityOfService().value()); + } else { + // option + final MqttSubscriptionOption option = topic.option(); + int optionEncoded = option.retainHandling().value() << 4; + if (option.isRetainAsPublished()) { + optionEncoded |= 0x08; + } + if (option.isNoLocal()) { + optionEncoded |= 0x04; + } + optionEncoded |= option.qos().value(); + buf.put((byte) optionEncoded); + } + } + return buf; + } + + private static ByteBuffer encodeUnsubscribeMessage(ChannelContext ctx, MqttUnSubscribeMessage message) { + MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + byte[] propertiesBytes = encodePropertiesIfNeeded(mqttVersion, + message.idAndPropertiesVariableHeader().properties()); + + final int variableHeaderBufferSize = 2 + propertiesBytes.length; + int payloadBufferSize = 0; + + MqttFixedHeader mqttFixedHeader = message.fixedHeader(); + MqttMessageIdVariableHeader variableHeader = message.variableHeader(); + MqttUnsubscribePayload payload = message.payload(); + + for (String topicName : payload.topics()) { + byte[] topicNameBytes = encodeStringUtf8(topicName); + payloadBufferSize += 2 + topicNameBytes.length; + } + + int variablePartSize = variableHeaderBufferSize + payloadBufferSize; + int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize); + + ByteBuffer buf = ByteBuffer.allocate(fixedHeaderBufferSize + variablePartSize); + buf.put(getFixedHeaderByte1(mqttFixedHeader)); + writeVariableLengthInt(buf, variablePartSize); + + // Variable Header + int messageId = variableHeader.messageId(); + buf.putShort((short) messageId); + buf.put(propertiesBytes); + + // Payload + for (String topicName : payload.topics()) { + // topicName + byte[] topicNameBytes = encodeStringUtf8(topicName); + buf.putShort((short) topicNameBytes.length); + buf.put(topicNameBytes, 0, topicNameBytes.length); + } + return buf; + } + + private static ByteBuffer encodeSubAckMessage(ChannelContext ctx, MqttSubAckMessage message) { + MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + byte[] propertiesBytes = encodePropertiesIfNeeded(mqttVersion, + message.idAndPropertiesVariableHeader().properties()); + int variableHeaderBufferSize = 2 + propertiesBytes.length; + int payloadBufferSize = message.payload().grantedQoSLevels().size(); + int variablePartSize = variableHeaderBufferSize + payloadBufferSize; + int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize); + ByteBuffer buf = ByteBuffer.allocate(fixedHeaderBufferSize + variablePartSize); + buf.put(getFixedHeaderByte1(message.fixedHeader())); + writeVariableLengthInt(buf, variablePartSize); + buf.putShort((short) message.variableHeader().messageId()); + buf.put(propertiesBytes); + for (Short code : message.payload().reasonCodes()) { + buf.put(code.byteValue()); + } + return buf; + } + + private static ByteBuffer encodeUnsubAckMessage(ChannelContext ctx, MqttUnSubAckMessage message) { + if (message.variableHeader() instanceof MqttMessageIdAndPropertiesVariableHeader) { + MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + byte[] propertiesBytes = encodePropertiesIfNeeded(mqttVersion, message.idAndPropertiesVariableHeader().properties()); + + int variableHeaderBufferSize = 2 + propertiesBytes.length; + MqttUnsubAckPayload payload = message.payload(); + int payloadBufferSize = payload == null ? 0 : payload.unsubscribeReasonCodes().size(); + int variablePartSize = variableHeaderBufferSize + payloadBufferSize; + int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize); + ByteBuffer buf = ByteBuffer.allocate(fixedHeaderBufferSize + variablePartSize); + buf.put(getFixedHeaderByte1(message.fixedHeader())); + writeVariableLengthInt(buf, variablePartSize); + buf.putShort((short) message.variableHeader().messageId()); + buf.put(propertiesBytes); + + if (payload != null) { + for (Short reasonCode : payload.unsubscribeReasonCodes()) { + buf.putShort(reasonCode); + } + } + return buf; + } else { + return encodeMessageWithOnlySingleByteFixedHeaderAndMessageId(message); + } + } + + private static ByteBuffer encodePublishMessage(ChannelContext ctx, MqttPublishMessage message) { + MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + MqttFixedHeader mqttFixedHeader = message.fixedHeader(); + MqttPublishVariableHeader variableHeader = message.variableHeader(); + byte[] payload = message.payload() == null ? ByteBufferUtil.EMPTY_BYTES : message.payload(); + + String topicName = variableHeader.topicName(); + byte[] topicNameBytes = encodeStringUtf8(topicName); + + byte[] propertiesBytes = encodePropertiesIfNeeded(mqttVersion, + message.variableHeader().properties()); + + int variableHeaderBufferSize = 2 + topicNameBytes.length + + (mqttFixedHeader.qosLevel().value() > 0 ? 2 : 0) + propertiesBytes.length; + int payloadBufferSize = payload.length; + int variablePartSize = variableHeaderBufferSize + payloadBufferSize; + int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize); + + ByteBuffer buf = ByteBuffer.allocate(fixedHeaderBufferSize + variablePartSize); + buf.put(getFixedHeaderByte1(mqttFixedHeader)); + writeVariableLengthInt(buf, variablePartSize); + buf.putShort((short) topicNameBytes.length); + buf.put(topicNameBytes); + if (mqttFixedHeader.qosLevel().value() > 0) { + buf.putShort((short) variableHeader.packetId()); + } + buf.put(propertiesBytes); + buf.put(payload); + return buf; + } + + private static ByteBuffer encodePubReplyMessage(ChannelContext ctx, MqttMessage message) { + if (message.variableHeader() instanceof MqttPubReplyMessageVariableHeader) { + MqttFixedHeader mqttFixedHeader = message.fixedHeader(); + MqttPubReplyMessageVariableHeader variableHeader = + (MqttPubReplyMessageVariableHeader) message.variableHeader(); + final byte[] propertiesBytes; + final boolean includeReasonCode; + final int variableHeaderBufferSize; + final MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + if (mqttVersion == MqttVersion.MQTT_5 && + (variableHeader.reasonCode() != MqttPubReplyMessageVariableHeader.REASON_CODE_OK || + !variableHeader.properties().isEmpty())) { + propertiesBytes = encodeProperties(variableHeader.properties()); + includeReasonCode = true; + variableHeaderBufferSize = 3 + propertiesBytes.length; + } else { + propertiesBytes = ByteBufferUtil.EMPTY_BYTES; + includeReasonCode = false; + variableHeaderBufferSize = 2; + } + + final int fixedHeaderBufferSize = 1 + getVariableLengthInt(variableHeaderBufferSize); + ByteBuffer buf = ByteBuffer.allocate(fixedHeaderBufferSize + variableHeaderBufferSize); + buf.put(getFixedHeaderByte1(mqttFixedHeader)); + writeVariableLengthInt(buf, variableHeaderBufferSize); + buf.putShort((short) variableHeader.messageId()); + if (includeReasonCode) { + buf.put(variableHeader.reasonCode()); + } + buf.put(propertiesBytes); + return buf; + } else { + return encodeMessageWithOnlySingleByteFixedHeaderAndMessageId(message); + } + } + + private static ByteBuffer encodeMessageWithOnlySingleByteFixedHeaderAndMessageId(MqttMessage message) { + MqttFixedHeader mqttFixedHeader = message.fixedHeader(); + MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) message.variableHeader(); + // variable part only has a message id + int variableHeaderBufferSize = 2; + int fixedHeaderBufferSize = 1 + getVariableLengthInt(variableHeaderBufferSize); + ByteBuffer buf = ByteBuffer.allocate(fixedHeaderBufferSize + variableHeaderBufferSize); + buf.put(getFixedHeaderByte1(mqttFixedHeader)); + writeVariableLengthInt(buf, variableHeaderBufferSize); + buf.putShort((short) variableHeader.messageId()); + return buf; + } + + private static ByteBuffer encodeReasonCodePlusPropertiesMessage(ChannelContext ctx, MqttMessage message) { + if (message.variableHeader() instanceof MqttReasonCodeAndPropertiesVariableHeader) { + MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(ctx); + MqttFixedHeader mqttFixedHeader = message.fixedHeader(); + MqttReasonCodeAndPropertiesVariableHeader variableHeader = + (MqttReasonCodeAndPropertiesVariableHeader) message.variableHeader(); + + final byte[] propertiesBytes; + final boolean includeReasonCode; + final int variableHeaderBufferSize; + if (mqttVersion == MqttVersion.MQTT_5 && + (variableHeader.reasonCode() != MqttReasonCodeAndPropertiesVariableHeader.REASON_CODE_OK || + !variableHeader.properties().isEmpty())) { + propertiesBytes = encodeProperties(variableHeader.properties()); + includeReasonCode = true; + variableHeaderBufferSize = 1 + propertiesBytes.length; + } else { + propertiesBytes = ByteBufferUtil.EMPTY_BYTES; + includeReasonCode = false; + variableHeaderBufferSize = 0; + } + final int fixedHeaderBufferSize = 1 + getVariableLengthInt(variableHeaderBufferSize); + ByteBuffer buf = ByteBuffer.allocate(fixedHeaderBufferSize + variableHeaderBufferSize); + buf.put(getFixedHeaderByte1(mqttFixedHeader)); + writeVariableLengthInt(buf, variableHeaderBufferSize); + if (includeReasonCode) { + buf.put(variableHeader.reasonCode()); + } + buf.put(propertiesBytes); + return buf; + } else { + return encodeMessageWithOnlySingleByteFixedHeader(message); + } + } + + private static ByteBuffer encodeMessageWithOnlySingleByteFixedHeader(MqttMessage message) { + MqttFixedHeader mqttFixedHeader = message.fixedHeader(); + ByteBuffer buf = ByteBuffer.allocate(2); + buf.put(getFixedHeaderByte1(mqttFixedHeader)); + buf.put((byte) 0); + return buf; + } + + private static byte[] encodePropertiesIfNeeded(MqttVersion mqttVersion, + MqttProperties mqttProperties) { + if (mqttVersion == MqttVersion.MQTT_5) { + return encodeProperties(mqttProperties); + } + return ByteBufferUtil.EMPTY_BYTES; + } + + private static byte[] encodeProperties(MqttProperties mqttProperties) { + FastByteBuffer writeBuffer = new FastByteBuffer(256); + for (MqttProperty property : mqttProperties.listAll()) { + int propertyId = property.propertyId(); + MqttPropertyType propertyType = MqttPropertyType.valueOf(propertyId); + switch (propertyType) { + case PAYLOAD_FORMAT_INDICATOR: + case REQUEST_PROBLEM_INFORMATION: + case REQUEST_RESPONSE_INFORMATION: + case MAXIMUM_QOS: + case RETAIN_AVAILABLE: + case WILDCARD_SUBSCRIPTION_AVAILABLE: + case SUBSCRIPTION_IDENTIFIER_AVAILABLE: + case SHARED_SUBSCRIPTION_AVAILABLE: + writeBuffer.writeVarLengthInt(propertyId); + final byte bytePropValue = ((IntegerProperty) property).value().byteValue(); + writeBuffer.writeByte(bytePropValue); + break; + case SERVER_KEEP_ALIVE: + case RECEIVE_MAXIMUM: + case TOPIC_ALIAS_MAXIMUM: + case TOPIC_ALIAS: + writeBuffer.writeVarLengthInt(propertyId); + final short twoBytesInPropValue = + ((IntegerProperty) property).value().shortValue(); + writeBuffer.writeShortBE(twoBytesInPropValue); + break; + case MESSAGE_EXPIRY_INTERVAL: + case SESSION_EXPIRY_INTERVAL: + case WILL_DELAY_INTERVAL: + case MAXIMUM_PACKET_SIZE: + writeBuffer.writeVarLengthInt(propertyId); + final int fourBytesIntPropValue = ((IntegerProperty) property).value(); + writeBuffer.writeIntBE(fourBytesIntPropValue); + break; + case SUBSCRIPTION_IDENTIFIER: + writeBuffer.writeVarLengthInt(propertyId); + final int vbi = ((IntegerProperty) property).value(); + writeBuffer.writeVarLengthInt(vbi); + break; + case CONTENT_TYPE: + case RESPONSE_TOPIC: + case ASSIGNED_CLIENT_IDENTIFIER: + case AUTHENTICATION_METHOD: + case RESPONSE_INFORMATION: + case SERVER_REFERENCE: + case REASON_STRING: + writeBuffer.writeVarLengthInt(propertyId); + writeEagerUTF8String(writeBuffer, ((StringProperty) property).value()); + break; + case USER_PROPERTY: + final List pairs = + ((UserProperties) property).value(); + for (StringPair pair : pairs) { + writeBuffer.writeVarLengthInt(propertyId); + writeEagerUTF8String(writeBuffer, pair.key); + writeEagerUTF8String(writeBuffer, pair.value); + } + break; + case CORRELATION_DATA: + case AUTHENTICATION_DATA: + writeBuffer.writeVarLengthInt(propertyId); + final byte[] binaryPropValue = ((BinaryProperty) property).value(); + writeBuffer.writeShortBE((short) binaryPropValue.length); + writeBuffer.writeBytes(binaryPropValue, 0, binaryPropValue.length); + break; + default: + //shouldn't reach here + throw new EncoderException("Unknown property type: " + propertyType); + } + } + byte[] propertiesBytes = writeBuffer.toArray(); + writeBuffer.reset(); + writeBuffer.writeVarLengthInt(propertiesBytes.length); + writeBuffer.writeBytes(propertiesBytes); + return writeBuffer.toArray(); + } + + private static byte getFixedHeaderByte1(MqttFixedHeader header) { + int ret = 0; + ret |= header.messageType().value() << 4; + if (header.isDup()) { + ret |= 0x08; + } + ret |= header.qosLevel().value() << 1; + if (header.isRetain()) { + ret |= 0x01; + } + return (byte) ret; + } + + private static void writeVariableLengthInt(ByteBuffer buf, int num) { + do { + int digit = num & 0x7F; + num >>>= 7; + if (num > 0) { + digit |= 0x80; + } + buf.put((byte) digit); + } while (num > 0); + } + + private static int getVariableLengthInt(int num) { + int count = 0; + do { + num >>>= 7; + count++; + } while (num > 0); + return count; + } + + private static void writeEagerUTF8String(FastByteBuffer buf, String s) { + if (s == null) { + buf.writeShortBE((short) 0); + } else { + byte[] bytes = s.getBytes(StandardCharsets.UTF_8); + buf.writeShortBE((short) bytes.length); + buf.writeBytes(bytes); + } + } + + private static byte[] encodeStringUtf8(String s) { + return s.getBytes(StandardCharsets.UTF_8); + } + + /** + * This is the main encoding method. + * It's only visible for testing. + * + * @param ctx ChannelContext + * @param message MQTT message to encode + * @return ByteBuf with encoded bytes + */ + public ByteBuffer doEncode(ChannelContext ctx, MqttMessage message) { + switch (message.fixedHeader().messageType()) { + case CONNECT: + return encodeConnectMessage(ctx, (MqttConnectMessage) message); + case CONNACK: + return encodeConnAckMessage(ctx, (MqttConnAckMessage) message); + case PUBLISH: + return encodePublishMessage(ctx, (MqttPublishMessage) message); + case SUBSCRIBE: + return encodeSubscribeMessage(ctx, (MqttSubscribeMessage) message); + case UNSUBSCRIBE: + return encodeUnsubscribeMessage(ctx, (MqttUnSubscribeMessage) message); + case SUBACK: + return encodeSubAckMessage(ctx, (MqttSubAckMessage) message); + case UNSUBACK: + if (message instanceof MqttUnSubAckMessage) { + return encodeUnsubAckMessage(ctx, (MqttUnSubAckMessage) message); + } + return encodeMessageWithOnlySingleByteFixedHeaderAndMessageId(message); + case PUBACK: + case PUBREC: + case PUBREL: + case PUBCOMP: + return encodePubReplyMessage(ctx, message); + case DISCONNECT: + case AUTH: + return encodeReasonCodePlusPropertiesMessage(ctx, message); + case PINGREQ: + case PINGRESP: + return encodeMessageWithOnlySingleByteFixedHeader(message); + default: + throw new IllegalArgumentException("Unknown message type: " + message.fixedHeader().messageType().value()); + } + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttMessageFactory.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttMessageFactory.java new file mode 100644 index 0000000..836000c --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttMessageFactory.java @@ -0,0 +1,86 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec; + +import org.dromara.mica.mqtt.codec.message.*; +import org.dromara.mica.mqtt.codec.message.header.*; +import org.dromara.mica.mqtt.codec.message.payload.*; + +/** + * Utility class with factory methods to create different types of MQTT messages. + * + * @author netty + */ +public final class MqttMessageFactory { + + private MqttMessageFactory() { + } + + public static MqttMessage newMessage(MqttFixedHeader mqttFixedHeader, Object variableHeader, Object payload) { + switch (mqttFixedHeader.messageType()) { + case CONNECT: + return new MqttConnectMessage( + mqttFixedHeader, + (MqttConnectVariableHeader) variableHeader, + (MqttConnectPayload) payload); + case CONNACK: + return new MqttConnAckMessage(mqttFixedHeader, (MqttConnAckVariableHeader) variableHeader); + case SUBSCRIBE: + return new MqttSubscribeMessage( + mqttFixedHeader, + (MqttMessageIdVariableHeader) variableHeader, + (MqttSubscribePayload) payload); + case SUBACK: + return new MqttSubAckMessage( + mqttFixedHeader, + (MqttMessageIdVariableHeader) variableHeader, + (MqttSubAckPayload) payload); + case UNSUBACK: + return new MqttUnSubAckMessage( + mqttFixedHeader, + (MqttMessageIdVariableHeader) variableHeader, + (MqttUnsubAckPayload) payload); + case UNSUBSCRIBE: + return new MqttUnSubscribeMessage( + mqttFixedHeader, + (MqttMessageIdVariableHeader) variableHeader, + (MqttUnsubscribePayload) payload); + case PUBLISH: + return new MqttPublishMessage( + mqttFixedHeader, + (MqttPublishVariableHeader) variableHeader, + (byte[]) payload); + case PUBACK: + //Having MqttPubReplyMessageVariableHeader or MqttMessageIdVariableHeader + return new MqttPubAckMessage(mqttFixedHeader, (MqttMessageIdVariableHeader) variableHeader); + case PUBREC: + case PUBREL: + case PUBCOMP: + //Having MqttPubReplyMessageVariableHeader or MqttMessageIdVariableHeader + return new MqttMessage(mqttFixedHeader, variableHeader); + case PINGREQ: + case PINGRESP: + return new MqttMessage(mqttFixedHeader); + case DISCONNECT: + case AUTH: + //Having MqttReasonCodeAndPropertiesVariableHeader + return new MqttMessage(mqttFixedHeader, variableHeader); + default: + throw new IllegalArgumentException("unknown message type: " + mqttFixedHeader.messageType()); + } + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttMessageType.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttMessageType.java new file mode 100644 index 0000000..0ab462d --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttMessageType.java @@ -0,0 +1,119 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec; + +/** + * MQTT Message Types. + * + * @author netty + */ +public enum MqttMessageType { + /** + * 连接服务端 + */ + CONNECT((byte) 1), + /** + * 确认连接请求 + */ + CONNACK((byte) 2), + /** + * 发布消息 + */ + PUBLISH((byte) 3), + /** + * 发布确认 + */ + PUBACK((byte) 4), + /** + * 发布收到(QoS 2,第一步) + */ + PUBREC((byte) 5), + /** + * 发布释放(QoS 2,第二步) + */ + PUBREL((byte) 6), + /** + * 发布完成(QoS 2,第三步) + */ + PUBCOMP((byte) 7), + /** + * 订阅主题 + */ + SUBSCRIBE((byte) 8), + /** + * 订阅确认 + */ + SUBACK((byte) 9), + /** + * 取消订阅 + */ + UNSUBSCRIBE((byte) 10), + /** + * 取消订阅确认 + */ + UNSUBACK((byte) 11), + /** + * 心跳请求 + */ + PINGREQ((byte) 12), + /** + * 心跳响应 + */ + PINGRESP((byte) 13), + /** + * 断开连接 + */ + DISCONNECT((byte) 14), + /** + * 认证 + */ + AUTH((byte) 15); + + private static final MqttMessageType[] VALUES; + + static { + // this prevent values to be assigned with the wrong order + // and ensure valueOf to work fine + final MqttMessageType[] values = values(); + VALUES = new MqttMessageType[values.length + 1]; + for (MqttMessageType mqttMessageType : values) { + final byte value = mqttMessageType.value; + if (VALUES[value] != null) { + throw new AssertionError("value already in use: " + value); + } + VALUES[value] = mqttMessageType; + } + } + + private final byte value; + + MqttMessageType(byte value) { + this.value = value; + } + + public static MqttMessageType valueOf(int type) { + if (type <= 0 || type >= VALUES.length) { + throw new IllegalArgumentException("unknown message type: " + type); + } + return VALUES[type]; + } + + public byte value() { + return value; + } +} + diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttQoS.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttQoS.java new file mode 100644 index 0000000..d2f8715 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttQoS.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package org.dromara.mica.mqtt.codec; + +/** + * Mqtt QoS + * + * @author netty + */ +public enum MqttQoS { + + /** + * QoS level 0 至多发送一次,发送即丢弃。没有确认消息,也不知道对方是否收到。 + */ + QOS0((byte) 0), + /** + * QoS level 1 至少一次,都要在可变头部中附加一个16位的消息ID,SUBSCRIBE 和 UNSUBSCRIBE 消息使用 QoS level 1。 + */ + QOS1((byte) 1), + /** + * QoS level 2 确保只有一次,仅仅在 PUBLISH 类型消息中出现,要求在可变头部中要附加消息ID。 + */ + QOS2((byte) 2), + /** + * 失败 + */ + FAILURE((byte) 0x80); + + private final byte value; + + MqttQoS(byte value) { + this.value = value; + } + + public static MqttQoS valueOf(int value) { + switch (value) { + case 0: + return QOS0; + case 1: + return QOS1; + case 2: + return QOS2; + case 0x80: + return FAILURE; + default: + throw new IllegalArgumentException("invalid QoS: " + value); + } + } + + public short value() { + return (short) (value & 0xFF); + } + + @Override + public String toString() { + return "QoS" + value; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttVersion.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttVersion.java new file mode 100644 index 0000000..b1b2d8e --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttVersion.java @@ -0,0 +1,83 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec; + +import org.dromara.mica.mqtt.codec.exception.MqttUnacceptableProtocolVersionException; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * Mqtt version specific constant values used by multiple classes in mqtt-codec. + * + * @author netty + */ +public enum MqttVersion { + /** + * mqtt 协议 + */ + MQTT_3_1("MQIsdp", (byte) 3, "MQTT 3.1"), + MQTT_3_1_1("MQTT", (byte) 4, "MQTT 3.1.1"), + MQTT_5("MQTT", (byte) 5, "MQTT 5.0"); + + private final String name; + private final byte level; + private final String fullName; + + MqttVersion(String protocolName, byte protocolLevel, String fullName) { + this.name = Objects.requireNonNull(protocolName, "protocolName is null."); + this.level = protocolLevel; + this.fullName = fullName; + } + + public static MqttVersion fromProtocolNameAndLevel(String protocolName, byte protocolLevel) { + MqttVersion mv; + switch (protocolLevel) { + case 3: + mv = MQTT_3_1; + break; + case 4: + mv = MQTT_3_1_1; + break; + case 5: + mv = MQTT_5; + break; + default: + throw new MqttUnacceptableProtocolVersionException(protocolName + " is an unknown protocol name"); + } + if (mv.name.equals(protocolName)) { + return mv; + } + throw new MqttUnacceptableProtocolVersionException(protocolName + " and " + protocolLevel + " don't match"); + } + + public String protocolName() { + return name; + } + + public byte[] protocolNameBytes() { + return name.getBytes(StandardCharsets.UTF_8); + } + + public byte protocolLevel() { + return level; + } + + public String fullName() { + return fullName; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttAuthReasonCode.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttAuthReasonCode.java new file mode 100644 index 0000000..cb56c48 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttAuthReasonCode.java @@ -0,0 +1,47 @@ +package org.dromara.mica.mqtt.codec.codes; + +/** + * Utilities for MQTT message codes enums + * + * @author vertx-mqtt + */ +public enum MqttAuthReasonCode implements MqttReasonCode { + + /** + * Success + */ + SUCCESS((byte) 0x0), + + /** + * Continue Authentication + */ + CONTINUE_AUTHENTICATION((byte) 0x18), + + /** + * Re-Authenticate + */ + RE_AUTHENTICATE((byte) 0x19); + + private final byte byteValue; + + MqttAuthReasonCode(byte byteValue) { + this.byteValue = byteValue; + } + + public static MqttAuthReasonCode valueOf(byte code) { + if (code == SUCCESS.byteValue) { + return SUCCESS; + } else if (code == CONTINUE_AUTHENTICATION.byteValue) { + return CONTINUE_AUTHENTICATION; + } else if (code == RE_AUTHENTICATE.byteValue) { + return RE_AUTHENTICATE; + } else { + throw new IllegalArgumentException("unknown AUTHENTICATE reason code: " + code); + } + } + + @Override + public byte value() { + return byteValue; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttConnectReasonCode.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttConnectReasonCode.java new file mode 100644 index 0000000..58e7dfe --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttConnectReasonCode.java @@ -0,0 +1,96 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.codes; + +import org.dromara.mica.mqtt.codec.message.MqttConnAckMessage; + +/** + * Return Code of {@link MqttConnAckMessage} + * + * @author netty + */ +public enum MqttConnectReasonCode implements MqttReasonCode { + CONNECTION_ACCEPTED((byte) 0x00, "连接已接受"), + + // MQTT 3 codes + CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION((byte) 0X01, "拒绝连接:不可接受的 mqtt 协议版本"), + CONNECTION_REFUSED_IDENTIFIER_REJECTED((byte) 0x02, "拒绝连接:clientId 标识符被拒绝"), + CONNECTION_REFUSED_SERVER_UNAVAILABLE((byte) 0x03, "拒绝连接:服务器不可用"), + CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD((byte) 0x04, "拒绝连接:用户名或密码错误"), + CONNECTION_REFUSED_NOT_AUTHORIZED((byte) 0x05, "拒绝连接:未经授权"), + + //MQTT 5 codes + CONNECTION_REFUSED_UNSPECIFIED_ERROR((byte) 0x80, "拒绝连接:未指明的错误"), + CONNECTION_REFUSED_MALFORMED_PACKET((byte) 0x81, "拒绝连接:报文格式错误"), + CONNECTION_REFUSED_PROTOCOL_ERROR((byte) 0x82, "拒绝连接:协议错误"), + CONNECTION_REFUSED_IMPLEMENTATION_SPECIFIC((byte) 0x83, "拒绝连接:实现特定错误"), + CONNECTION_REFUSED_UNSUPPORTED_PROTOCOL_VERSION((byte) 0x84, "拒绝连接:不支持的协议版本"), + CONNECTION_REFUSED_CLIENT_IDENTIFIER_NOT_VALID((byte) 0x85, "拒绝连接:客户端标识符无效"), + CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD((byte) 0x86, "拒绝连接:用户名或密码错误"), + CONNECTION_REFUSED_NOT_AUTHORIZED_5((byte) 0x87, "拒绝连接:未经授权"), + CONNECTION_REFUSED_SERVER_UNAVAILABLE_5((byte) 0x88, "拒绝连接:服务器不可用"), + CONNECTION_REFUSED_SERVER_BUSY((byte) 0x89, "拒绝连接:服务器忙"), + CONNECTION_REFUSED_BANNED((byte) 0x8A, "拒绝连接:被禁止"), + CONNECTION_REFUSED_BAD_AUTHENTICATION_METHOD((byte) 0x8C, "拒绝连接:认证方法错误"), + CONNECTION_REFUSED_TOPIC_NAME_INVALID((byte) 0x90, "拒绝连接:主题名无效"), + CONNECTION_REFUSED_PACKET_TOO_LARGE((byte) 0x95, "拒绝连接:报文过大"), + CONNECTION_REFUSED_QUOTA_EXCEEDED((byte) 0x97, "拒绝连接:超出配额"), + CONNECTION_REFUSED_PAYLOAD_FORMAT_INVALID((byte) 0x99, "拒绝连接:有效负载格式无效"), + CONNECTION_REFUSED_RETAIN_NOT_SUPPORTED((byte) 0x9A, "拒绝连接:不支持保留"), + CONNECTION_REFUSED_QOS_NOT_SUPPORTED((byte) 0x9B, "拒绝连接:不支持服务质量"), + CONNECTION_REFUSED_USE_ANOTHER_SERVER((byte) 0x9C, "拒绝连接:请使用其他服务器"), + CONNECTION_REFUSED_SERVER_MOVED((byte) 0x9D, "拒绝连接:服务器已移动"), + CONNECTION_REFUSED_CONNECTION_RATE_EXCEEDED((byte) 0x9F, "拒绝连接:连接速率超出限制"); + + private static final MqttConnectReasonCode[] VALUES = new MqttConnectReasonCode[160]; + + static { + ReasonCodeUtils.fillValuesByCode(VALUES, values()); + } + + private final byte byteValue; + private final String message; + + MqttConnectReasonCode(byte byteValue, String message) { + this.byteValue = byteValue; + this.message = message; + } + + public static MqttConnectReasonCode valueOf(byte b) { + return ReasonCodeUtils.codeLoopUp(VALUES, b, "Connect"); + } + + /** + * 是否接收 + * + * @return 是否已接受 + */ + public boolean isAccepted() { + return CONNECTION_ACCEPTED == this; + } + + @Override + public byte value() { + return byteValue; + } + + @Override + public String toString() { + return this.name().toLowerCase().replace('_', ' ') + " (" + this.message + ')'; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttDisconnectReasonCode.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttDisconnectReasonCode.java new file mode 100644 index 0000000..a5f5153 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttDisconnectReasonCode.java @@ -0,0 +1,79 @@ +/* + * Copyright 2021 The vertx Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.codes; + +/** + * Reason codes for DISCONNECT MQTT message + * + * @author vertx-mqtt + */ +public enum MqttDisconnectReasonCode implements MqttReasonCode { + + /** + * Disconnect ReasonCode + */ + NORMAL((byte) 0x0), + WITH_WILL_MESSAGE((byte) 0x04), + UNSPECIFIED_ERROR((byte) 0x80), + MALFORMED_PACKET((byte) 0x81), + PROTOCOL_ERROR((byte) 0x82), + IMPLEMENTATION_SPECIFIC_ERROR((byte) 0x83), + NOT_AUTHORIZED((byte) 0x87), + SERVER_BUSY((byte) 0x89), + SERVER_SHUTTING_DOWN((byte) 0x8B), + KEEP_ALIVE_TIMEOUT((byte) 0x8D), + SESSION_TAKEN_OVER((byte) 0x8E), + TOPIC_FILTER_INVALID((byte) 0x8F), + TOPIC_NAME_INVALID((byte) 0x90), + RECEIVE_MAXIMUM_EXCEEDED((byte) 0x93), + TOPIC_ALIAS_INVALID((byte) 0x94), + PACKET_TOO_LARGE((byte) 0x95), + MESSAGE_RATE_TOO_HIGH((byte) 0x96), + QUOTA_EXCEEDED((byte) 0x97), + ADMINISTRATIVE_ACTION((byte) 0x98), + PAYLOAD_FORMAT_INVALID((byte) 0x99), + RETAIN_NOT_SUPPORTED((byte) 0x9A), + QOS_NOT_SUPPORTED((byte) 0x9B), + USE_ANOTHER_SERVER((byte) 0x9C), + SERVER_MOVED((byte) 0x9D), + SHARED_SUBSCRIPTIONS_NOT_SUPPORTED((byte) 0x9E), + CONNECTION_RATE_EXCEEDED((byte) 0x9F), + MAXIMUM_CONNECT_TIME((byte) 0xA0), + SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED((byte) 0xA1), + WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED((byte) 0xA2); + + private static final MqttDisconnectReasonCode[] VALUES = new MqttDisconnectReasonCode[0xA3]; + + static { + ReasonCodeUtils.fillValuesByCode(VALUES, values()); + } + + private final byte byteValue; + + MqttDisconnectReasonCode(byte byteValue) { + this.byteValue = byteValue; + } + + public static MqttDisconnectReasonCode valueOf(byte b) { + return ReasonCodeUtils.codeLoopUp(VALUES, b, "DISCONNECT"); + } + + @Override + public byte value() { + return byteValue; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubAckReasonCode.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubAckReasonCode.java new file mode 100644 index 0000000..9a66981 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubAckReasonCode.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 The vertx Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.codes; + +/** + * Reason codes for PUBACK MQTT message + * + * @author vertx-mqtt + */ +public enum MqttPubAckReasonCode implements MqttReasonCode { + + /** + * PubAck ReasonCode + */ + SUCCESS((byte) 0x0), + NO_MATCHING_SUBSCRIBERS((byte) 0x10), + UNSPECIFIED_ERROR((byte) 0x80), + IMPLEMENTATION_SPECIFIC_ERROR((byte) 0x83), + NOT_AUTHORIZED((byte) 0x87), + TOPIC_NAME_INVALID((byte) 0x90), + PACKET_IDENTIFIER_IN_USE((byte) 0x91), + QUOTA_EXCEEDED((byte) 0x97), + PAYLOAD_FORMAT_INVALID((byte) 0x99); + + private static final MqttPubAckReasonCode[] VALUES = new MqttPubAckReasonCode[0x9A]; + + static { + ReasonCodeUtils.fillValuesByCode(VALUES, values()); + } + + private final byte byteValue; + + MqttPubAckReasonCode(byte byteValue) { + this.byteValue = byteValue; + } + + public static MqttPubAckReasonCode valueOf(byte b) { + return ReasonCodeUtils.codeLoopUp(VALUES, b, "PUBACK"); + } + + @Override + public byte value() { + return byteValue; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubCompReasonCode.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubCompReasonCode.java new file mode 100644 index 0000000..b6f23dc --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubCompReasonCode.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 The vertx Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.codes; + +/** + * Reason codes for PUBCOMP MQTT message + * + * @author vertx-mqtt + */ +public enum MqttPubCompReasonCode implements MqttReasonCode { + + /** + * PubComp ReasonCode + */ + SUCCESS((byte) 0x0), + PACKET_IDENTIFIER_NOT_FOUND((byte) 0x92); + + private final byte byteValue; + + MqttPubCompReasonCode(byte byteValue) { + this.byteValue = byteValue; + } + + public static MqttPubCompReasonCode valueOf(byte b) { + if (b == SUCCESS.byteValue) { + return SUCCESS; + } else if (b == PACKET_IDENTIFIER_NOT_FOUND.byteValue) { + return PACKET_IDENTIFIER_NOT_FOUND; + } else { + throw new IllegalArgumentException("unknown PUBCOMP reason code: " + b); + } + } + + @Override + public byte value() { + return byteValue; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubRecReasonCode.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubRecReasonCode.java new file mode 100644 index 0000000..ee43ffb --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubRecReasonCode.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021 The vertx Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.codes; + +/** + * Reason codes for PUBREC MQTT message + * + * @author vertx-mqtt + */ +public enum MqttPubRecReasonCode implements MqttReasonCode { + + /** + * PubRec ReasonCode + */ + SUCCESS((byte) 0x0), + NO_MATCHING_SUBSCRIBERS((byte) 0x10), + UNSPECIFIED_ERROR((byte) 0x80), + IMPLEMENTATION_SPECIFIC_ERROR((byte) 0x83), + NOT_AUTHORIZED((byte) 0x87), + TOPIC_NAME_INVALID((byte) 0x90), + PACKET_IDENTIFIER_IN_USE((byte) 0x91), + QUOTA_EXCEEDED((byte) 0x97), + PAYLOAD_FORMAT_INVALID((byte) 0x99); + + private static final MqttPubRecReasonCode[] VALUES = new MqttPubRecReasonCode[0x9A]; + + static { + ReasonCodeUtils.fillValuesByCode(VALUES, values()); + } + + private final byte byteValue; + + MqttPubRecReasonCode(byte byteValue) { + this.byteValue = byteValue; + } + + public static MqttPubRecReasonCode valueOf(byte b) { + return ReasonCodeUtils.codeLoopUp(VALUES, b, "PUBREC"); + } + + @Override + public byte value() { + return byteValue; + } + + @Override + public boolean isError() { + return Byte.toUnsignedInt(byteValue) >= Byte.toUnsignedInt(UNSPECIFIED_ERROR.byteValue); + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubRelReasonCode.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubRelReasonCode.java new file mode 100644 index 0000000..9b037d6 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubRelReasonCode.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 The vertx Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.codes; + +/** + * Reason codes for PUBREL MQTT message + * + * @author vertx-mqtt + */ +public enum MqttPubRelReasonCode implements MqttReasonCode { + + /** + * PubRel ReasonCode + */ + SUCCESS((byte) 0x0), + PACKET_IDENTIFIER_NOT_FOUND((byte) 0x92); + + private final byte byteValue; + + MqttPubRelReasonCode(byte byteValue) { + this.byteValue = byteValue; + } + + public static MqttPubRelReasonCode valueOf(byte b) { + if (b == SUCCESS.byteValue) { + return SUCCESS; + } else if (b == PACKET_IDENTIFIER_NOT_FOUND.byteValue) { + return PACKET_IDENTIFIER_NOT_FOUND; + } else { + throw new IllegalArgumentException("unknown PUBREL reason code: " + b); + } + } + + @Override + public byte value() { + return byteValue; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttReasonCode.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttReasonCode.java new file mode 100644 index 0000000..92d0cdb --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttReasonCode.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 The vertx Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.codes; + +/** + * Common interface for MQTT messages reason codes enums + * + * @author vertx-mqtt + */ +public interface MqttReasonCode { + + /** + * byteValue + * + * @return byteValue + */ + byte value(); + + /** + * isError + * + * @return boolean + */ + default boolean isError() { + return (value() & 0x80) != 0; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttSubAckReasonCode.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttSubAckReasonCode.java new file mode 100644 index 0000000..efae38e --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttSubAckReasonCode.java @@ -0,0 +1,77 @@ +/* + * Copyright 2021 The vertx Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.codes; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.MqttVersion; + +/** + * Reason codes for SUBACK MQTT message + * + * @author vertx-mqtt + */ +public enum MqttSubAckReasonCode implements MqttReasonCode { + + //All MQTT versions + GRANTED_QOS0((byte) 0x0), + GRANTED_QOS1((byte) 0x1), + GRANTED_QOS2((byte) 0x2), + UNSPECIFIED_ERROR((byte) 0x80), + //MQTT5 or higher + IMPLEMENTATION_SPECIFIC_ERROR((byte) 0x83), + NOT_AUTHORIZED((byte) 0x87), + TOPIC_FILTER_INVALID((byte) 0x8F), + PACKET_IDENTIFIER_IN_USE((byte) 0x91), + QUOTA_EXCEEDED((byte) 0x97), + SHARED_SUBSCRIPTIONS_NOT_SUPPORTED((byte) 0x9E), + SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED((byte) 0xA1), + WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED((byte) 0xA2); + + private final byte byteValue; + + MqttSubAckReasonCode(byte byteValue) { + this.byteValue = byteValue; + } + + public static MqttSubAckReasonCode qosGranted(MqttQoS qos) { + switch (qos) { + case QOS0: + return MqttSubAckReasonCode.GRANTED_QOS0; + case QOS1: + return MqttSubAckReasonCode.GRANTED_QOS1; + case QOS2: + return MqttSubAckReasonCode.GRANTED_QOS2; + case FAILURE: + return MqttSubAckReasonCode.UNSPECIFIED_ERROR; + default: + return MqttSubAckReasonCode.UNSPECIFIED_ERROR; + } + } + + @Override + public byte value() { + return byteValue; + } + + public MqttSubAckReasonCode limitForMqttVersion(MqttVersion version) { + if (version != MqttVersion.MQTT_5 && byteValue > UNSPECIFIED_ERROR.byteValue) { + return UNSPECIFIED_ERROR; + } else { + return this; + } + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttUnSubAckReasonCode.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttUnSubAckReasonCode.java new file mode 100644 index 0000000..c0dd34a --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttUnSubAckReasonCode.java @@ -0,0 +1,48 @@ +/* + * Copyright 2021 The vertx Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.codes; + +/** + * Reason codes for UNSUBACK MQTT message + * + * @author vertx-mqtt + */ +public enum MqttUnSubAckReasonCode implements MqttReasonCode { + + /** + * UnsubAck ReasonCode + */ + SUCCESS((byte) 0x0), + NO_SUBSCRIPTION_EXISTED((byte) 0x11), + UNSPECIFIED_ERROR((byte) 0x80), + IMPLEMENTATION_SPECIFIC_ERROR((byte) 0x83), + NOT_AUTHORIZED((byte) 0x87), + TOPIC_FILTER_INVALID((byte) 0x8F), + PACKET_IDENTIFIER_IN_USE((byte) 0x91); + + private final byte byteValue; + + MqttUnSubAckReasonCode(byte byteValue) { + this.byteValue = byteValue; + } + + @Override + public byte value() { + return byteValue; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/ReasonCodeUtils.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/ReasonCodeUtils.java new file mode 100644 index 0000000..73d27f2 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/ReasonCodeUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 The vertx Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.codes; + +/** + * Utilities for MQTT message codes enums + * + * @author vertx-mqtt + */ +public class ReasonCodeUtils { + + protected static void fillValuesByCode(C[] valuesByCode, C[] values) { + for (C code : values) { + final int unsignedByte = code.value() & 0xFF; + valuesByCode[unsignedByte] = code; + } + } + + protected static C codeLoopUp(C[] valuesByCode, byte b, String codeType) { + final int unsignedByte = b & 0xFF; + C reasonCode = null; + try { + reasonCode = valuesByCode[unsignedByte]; + } catch (ArrayIndexOutOfBoundsException ignored) { + // no op + } + if (reasonCode == null) { + throw new IllegalArgumentException("unknown " + codeType + " reason code: " + unsignedByte); + } + return reasonCode; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/DecoderException.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/DecoderException.java new file mode 100644 index 0000000..4e17409 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/DecoderException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.exception; + +/** + * 解码异常 + * + * @author L.cm + */ +public class DecoderException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public DecoderException() { + } + + public DecoderException(String message) { + super(message); + } + + public DecoderException(String message, Throwable cause) { + super(message, cause); + } + + public DecoderException(Throwable cause) { + super(cause); + } + + public DecoderException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/EncoderException.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/EncoderException.java new file mode 100644 index 0000000..ccd2067 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/EncoderException.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.exception; + +/** + * An {@link RuntimeException} which is thrown by an encoder. + * + * @author netty + */ +public class EncoderException extends RuntimeException { + + private static final long serialVersionUID = -5086121160476476774L; + + /** + * Creates a new instance. + */ + public EncoderException() { + } + + /** + * Creates a new instance. + * + * @param message message + * @param cause Throwable + */ + public EncoderException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new instance. + * + * @param message message + */ + public EncoderException(String message) { + super(message); + } + + /** + * Creates a new instance. + * + * @param cause Throwable + */ + public EncoderException(Throwable cause) { + super(cause); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/MqttIdentifierRejectedException.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/MqttIdentifierRejectedException.java new file mode 100644 index 0000000..c05107f --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/MqttIdentifierRejectedException.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.dromara.mica.mqtt.codec.exception; + +/** + * A {@link MqttIdentifierRejectedException} which is thrown when a CONNECT request contains invalid client identifier. + * + * @author netty + */ +public final class MqttIdentifierRejectedException extends DecoderException { + private static final long serialVersionUID = -1323503322689614981L; + + /** + * Creates a new instance + */ + public MqttIdentifierRejectedException() { + } + + /** + * Creates a new instance + * + * @param message message + * @param cause Throwable + */ + public MqttIdentifierRejectedException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new instance + * + * @param message message + */ + public MqttIdentifierRejectedException(String message) { + super(message); + } + + /** + * Creates a new instance + * + * @param cause Throwable + */ + public MqttIdentifierRejectedException(Throwable cause) { + super(cause); + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/MqttUnacceptableProtocolVersionException.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/MqttUnacceptableProtocolVersionException.java new file mode 100644 index 0000000..f7573f5 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/MqttUnacceptableProtocolVersionException.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.exception; + +/** + * A {@link MqttUnacceptableProtocolVersionException} which is thrown when + * a CONNECT request contains unacceptable protocol version. + * + * @author netty + */ +public final class MqttUnacceptableProtocolVersionException extends DecoderException { + private static final long serialVersionUID = 4914652213232455749L; + + /** + * Creates a new instance + */ + public MqttUnacceptableProtocolVersionException() { + } + + /** + * Creates a new instance + * + * @param message message + * @param cause Throwable + */ + public MqttUnacceptableProtocolVersionException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new instance + * + * @param message message + */ + public MqttUnacceptableProtocolVersionException(String message) { + super(message); + } + + /** + * Creates a new instance + * + * @param cause Throwable + */ + public MqttUnacceptableProtocolVersionException(Throwable cause) { + super(cause); + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttConnAckMessage.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttConnAckMessage.java new file mode 100644 index 0000000..7f3ee2f --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttConnAckMessage.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message; + +import org.dromara.mica.mqtt.codec.message.builder.MqttConnAckBuilder; +import org.dromara.mica.mqtt.codec.message.header.MqttConnAckVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; + +/** + * See MQTTV3.1/connack + * + * @author netty + */ +public final class MqttConnAckMessage extends MqttMessage { + + public MqttConnAckMessage(MqttFixedHeader mqttFixedHeader, MqttConnAckVariableHeader variableHeader) { + super(mqttFixedHeader, variableHeader); + } + + @Override + public MqttConnAckVariableHeader variableHeader() { + return (MqttConnAckVariableHeader) super.variableHeader(); + } + + /** + * Create a builder for a {@link MqttConnAckMessage} + * + * @return a new instance of {@link MqttConnAckBuilder} + */ + public static MqttConnAckBuilder builder() { + return new MqttConnAckBuilder(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttConnectMessage.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttConnectMessage.java new file mode 100644 index 0000000..7c2b44a --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttConnectMessage.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message; + +import org.dromara.mica.mqtt.codec.message.builder.MqttConnectBuilder; +import org.dromara.mica.mqtt.codec.message.header.MqttConnectVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttConnectPayload; + +/** + * See MQTTV3.1/connect + * + * @author netty + */ +public final class MqttConnectMessage extends MqttMessage { + + public MqttConnectMessage( + MqttFixedHeader mqttFixedHeader, + MqttConnectVariableHeader variableHeader, + MqttConnectPayload payload) { + super(mqttFixedHeader, variableHeader, payload); + } + + @Override + public MqttConnectVariableHeader variableHeader() { + return (MqttConnectVariableHeader) super.variableHeader(); + } + + @Override + public MqttConnectPayload payload() { + return (MqttConnectPayload) super.payload(); + } + + /** + * Create a builder for a {@link MqttConnectMessage} + * + * @return a new instance of {@link MqttConnectBuilder} + */ + public static MqttConnectBuilder builder() { + return new MqttConnectBuilder(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttMessage.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttMessage.java new file mode 100644 index 0000000..7c34b7c --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttMessage.java @@ -0,0 +1,77 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.tio.core.intf.Packet; + +/** + * Base class for all MQTT message types. + * + * @author netty + */ +public class MqttMessage extends Packet { + // Constants for fixed-header only message types with all flags set to 0 (see + // https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.2_-) + public static final MqttMessage PINGREQ = new MqttMessage(new MqttFixedHeader(MqttMessageType.PINGREQ, false, + MqttQoS.QOS0, false, 0)); + public static final MqttMessage PINGRESP = new MqttMessage(new MqttFixedHeader(MqttMessageType.PINGRESP, false, + MqttQoS.QOS0, false, 0)); + public static final MqttMessage DISCONNECT = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, + MqttQoS.QOS0, false, 0)); + + private final MqttFixedHeader mqttFixedHeader; + private final Object variableHeader; + private final Object payload; + + public MqttMessage(MqttFixedHeader mqttFixedHeader) { + this(mqttFixedHeader, null, null); + } + + public MqttMessage(MqttFixedHeader mqttFixedHeader, Object variableHeader) { + this(mqttFixedHeader, variableHeader, null); + } + + public MqttMessage(MqttFixedHeader mqttFixedHeader, Object variableHeader, Object payload) { + this.mqttFixedHeader = mqttFixedHeader; + this.variableHeader = variableHeader; + this.payload = payload; + } + + public MqttFixedHeader fixedHeader() { + return mqttFixedHeader; + } + + public Object variableHeader() { + return variableHeader; + } + + public Object payload() { + return payload; + } + + @Override + public String toString() { + return "MqttMessage[" + + "fixedHeader=" + (fixedHeader() != null ? fixedHeader().toString() : "") + + ", variableHeader=" + (variableHeader() != null ? variableHeader.toString() : "") + + ", payload=" + (payload() != null ? payload.toString() : "") + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttPubAckMessage.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttPubAckMessage.java new file mode 100644 index 0000000..ada876e --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttPubAckMessage.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message; + +import org.dromara.mica.mqtt.codec.message.builder.MqttPubAckBuilder; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdVariableHeader; + +/** + * See MQTTV3.1/puback + * + * @author netty + */ +public final class MqttPubAckMessage extends MqttMessage { + + public MqttPubAckMessage(MqttFixedHeader mqttFixedHeader, MqttMessageIdVariableHeader variableHeader) { + super(mqttFixedHeader, variableHeader); + } + + @Override + public MqttMessageIdVariableHeader variableHeader() { + return (MqttMessageIdVariableHeader) super.variableHeader(); + } + + /** + * Create a builder for a {@link MqttPubAckMessage} + * + * @return a new instance of {@link MqttPubAckBuilder} + */ + public static MqttPubAckBuilder builder() { + return new MqttPubAckBuilder(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttPublishMessage.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttPublishMessage.java new file mode 100644 index 0000000..54f993d --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttPublishMessage.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message; + +import org.dromara.mica.mqtt.codec.message.builder.MqttPublishBuilder; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttPublishVariableHeader; + +/** + * See MQTTV3.1/publish + * + * @author netty + */ +public class MqttPublishMessage extends MqttMessage { + public MqttPublishMessage( + MqttFixedHeader mqttFixedHeader, + MqttPublishVariableHeader variableHeader, + byte[] payload) { + super(mqttFixedHeader, variableHeader, payload); + } + + @Override + public MqttPublishVariableHeader variableHeader() { + return (MqttPublishVariableHeader) super.variableHeader(); + } + + @Override + public byte[] payload() { + return (byte[]) super.payload(); + } + + public byte[] getPayload() { + return (byte[]) super.payload(); + } + + /** + * Create a builder for a {@link MqttPublishMessage} + * + * @return a new instance of {@link MqttPublishBuilder} + */ + public static MqttPublishBuilder builder() { + return new MqttPublishBuilder(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttSubAckMessage.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttSubAckMessage.java new file mode 100644 index 0000000..8e0c431 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttSubAckMessage.java @@ -0,0 +1,68 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message; + +import org.dromara.mica.mqtt.codec.message.builder.MqttSubAckBuilder; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdAndPropertiesVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdVariableHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttSubAckPayload; + +/** + * See MQTTV3.1/suback + * + * @author netty + */ +public final class MqttSubAckMessage extends MqttMessage { + + public MqttSubAckMessage( + MqttFixedHeader mqttFixedHeader, + MqttMessageIdAndPropertiesVariableHeader variableHeader, + MqttSubAckPayload payload) { + super(mqttFixedHeader, variableHeader, payload); + } + + public MqttSubAckMessage( + MqttFixedHeader mqttFixedHeader, + MqttMessageIdVariableHeader variableHeader, + MqttSubAckPayload payload) { + this(mqttFixedHeader, variableHeader.withDefaultEmptyProperties(), payload); + } + + @Override + public MqttMessageIdVariableHeader variableHeader() { + return (MqttMessageIdVariableHeader) super.variableHeader(); + } + + public MqttMessageIdAndPropertiesVariableHeader idAndPropertiesVariableHeader() { + return (MqttMessageIdAndPropertiesVariableHeader) super.variableHeader(); + } + + @Override + public MqttSubAckPayload payload() { + return (MqttSubAckPayload) super.payload(); + } + + /** + * Create a builder for a {@link MqttSubAckMessage} + * + * @return a new instance of {@link MqttSubAckBuilder} + */ + public static MqttSubAckBuilder builder() { + return new MqttSubAckBuilder(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttSubscribeMessage.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttSubscribeMessage.java new file mode 100644 index 0000000..00afc9f --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttSubscribeMessage.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message; + +import org.dromara.mica.mqtt.codec.message.builder.MqttSubscribeBuilder; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdAndPropertiesVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdVariableHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttSubscribePayload; + +/** + * See + * MQTTV3.1/subscribe + * + * @author netty + */ +public final class MqttSubscribeMessage extends MqttMessage { + + public MqttSubscribeMessage( + MqttFixedHeader mqttFixedHeader, + MqttMessageIdAndPropertiesVariableHeader variableHeader, + MqttSubscribePayload payload) { + super(mqttFixedHeader, variableHeader, payload); + } + + public MqttSubscribeMessage( + MqttFixedHeader mqttFixedHeader, + MqttMessageIdVariableHeader variableHeader, + MqttSubscribePayload payload) { + this(mqttFixedHeader, variableHeader.withDefaultEmptyProperties(), payload); + } + + @Override + public MqttMessageIdVariableHeader variableHeader() { + return (MqttMessageIdVariableHeader) super.variableHeader(); + } + + public MqttMessageIdAndPropertiesVariableHeader idAndPropertiesVariableHeader() { + return (MqttMessageIdAndPropertiesVariableHeader) super.variableHeader(); + } + + @Override + public MqttSubscribePayload payload() { + return (MqttSubscribePayload) super.payload(); + } + + /** + * Create a builder for a {@link MqttSubscribeMessage} + * + * @return a new instance of {@link MqttSubscribeBuilder} + */ + public static MqttSubscribeBuilder builder() { + return new MqttSubscribeBuilder(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttUnSubAckMessage.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttUnSubAckMessage.java new file mode 100644 index 0000000..c8e5ec8 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttUnSubAckMessage.java @@ -0,0 +1,81 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message; + +import org.dromara.mica.mqtt.codec.message.builder.MqttUnSubAckBuilder; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdAndPropertiesVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdVariableHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttUnsubAckPayload; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +/** + * See + * MQTTV3.1/unsuback + * + * @author netty + */ +public final class MqttUnSubAckMessage extends MqttMessage { + + public MqttUnSubAckMessage(MqttFixedHeader mqttFixedHeader, + MqttMessageIdAndPropertiesVariableHeader variableHeader, + MqttUnsubAckPayload payload) { + super(mqttFixedHeader, variableHeader, MqttUnsubAckPayload.withEmptyDefaults(payload)); + } + + public MqttUnSubAckMessage(MqttFixedHeader mqttFixedHeader, + MqttMessageIdVariableHeader variableHeader, + MqttUnsubAckPayload payload) { + this(mqttFixedHeader, fallbackVariableHeader(variableHeader), payload); + } + + public MqttUnSubAckMessage(MqttFixedHeader mqttFixedHeader, + MqttMessageIdVariableHeader variableHeader) { + this(mqttFixedHeader, variableHeader, null); + } + + private static MqttMessageIdAndPropertiesVariableHeader fallbackVariableHeader( + MqttMessageIdVariableHeader variableHeader) { + if (variableHeader instanceof MqttMessageIdAndPropertiesVariableHeader) { + return (MqttMessageIdAndPropertiesVariableHeader) variableHeader; + } + return new MqttMessageIdAndPropertiesVariableHeader(variableHeader.messageId(), MqttProperties.NO_PROPERTIES); + } + + @Override + public MqttMessageIdVariableHeader variableHeader() { + return (MqttMessageIdVariableHeader) super.variableHeader(); + } + + public MqttMessageIdAndPropertiesVariableHeader idAndPropertiesVariableHeader() { + return (MqttMessageIdAndPropertiesVariableHeader) super.variableHeader(); + } + + @Override + public MqttUnsubAckPayload payload() { + return (MqttUnsubAckPayload) super.payload(); + } + + /** + * Create a builder for a {@link MqttUnSubAckMessage} + * + * @return a new instance of {@link MqttUnSubAckBuilder} + */ + public static MqttUnSubAckBuilder builder() { + return new MqttUnSubAckBuilder(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttUnSubscribeMessage.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttUnSubscribeMessage.java new file mode 100644 index 0000000..3490fff --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttUnSubscribeMessage.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message; + +import org.dromara.mica.mqtt.codec.message.builder.MqttUnSubscribeBuilder; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdAndPropertiesVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdVariableHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttUnsubscribePayload; + +/** + * See + * MQTTV3.1/unsubscribe + * + * @author netty + */ +public final class MqttUnSubscribeMessage extends MqttMessage { + + public MqttUnSubscribeMessage( + MqttFixedHeader mqttFixedHeader, + MqttMessageIdAndPropertiesVariableHeader variableHeader, + MqttUnsubscribePayload payload) { + super(mqttFixedHeader, variableHeader, payload); + } + + public MqttUnSubscribeMessage( + MqttFixedHeader mqttFixedHeader, + MqttMessageIdVariableHeader variableHeader, + MqttUnsubscribePayload payload) { + this(mqttFixedHeader, variableHeader.withDefaultEmptyProperties(), payload); + } + + @Override + public MqttMessageIdVariableHeader variableHeader() { + return (MqttMessageIdVariableHeader) super.variableHeader(); + } + + public MqttMessageIdAndPropertiesVariableHeader idAndPropertiesVariableHeader() { + return (MqttMessageIdAndPropertiesVariableHeader) super.variableHeader(); + } + + @Override + public MqttUnsubscribePayload payload() { + return (MqttUnsubscribePayload) super.payload(); + } + + /** + * Create a builder for a {@link MqttUnSubscribeMessage} + * + * @return a new instance of {@link MqttUnSubscribeBuilder} + */ + public static MqttUnSubscribeBuilder builder() { + return new MqttUnSubscribeBuilder(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttAuthBuilder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttAuthBuilder.java new file mode 100644 index 0000000..f72eee5 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttAuthBuilder.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.codes.MqttAuthReasonCode; +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttReasonCodeAndPropertiesVariableHeader; +import org.dromara.mica.mqtt.codec.message.properties.MqttAuthProperties; +import org.dromara.mica.mqtt.codec.properties.*; + +import java.util.function.Consumer; + +/** + * MqttAuthMessage builder + * + * @author netty, L.cm + */ +public final class MqttAuthBuilder { + private MqttAuthReasonCode reasonCode; + private MqttProperties properties = MqttProperties.NO_PROPERTIES; + + public MqttAuthBuilder() { + } + + public MqttAuthBuilder reasonCode(MqttAuthReasonCode reasonCode) { + this.reasonCode = reasonCode; + return this; + } + + public MqttAuthBuilder properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttAuthBuilder properties(Consumer consumer) { + MqttAuthProperties authProperties = new MqttAuthProperties(); + consumer.accept(authProperties); + return properties(authProperties.getProperties()); + } + + /** + * 构建 MqttAuthMessage + * + * @return MqttAuthMessage + */ + public MqttMessage build() { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.AUTH, false, MqttQoS.QOS0, false, 0); + MqttReasonCodeAndPropertiesVariableHeader mqttAuthVariableHeader = + new MqttReasonCodeAndPropertiesVariableHeader(reasonCode.value(), properties); + return new MqttMessage(mqttFixedHeader, mqttAuthVariableHeader); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttConnAckBuilder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttConnAckBuilder.java new file mode 100644 index 0000000..f36126b --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttConnAckBuilder.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.codes.MqttConnectReasonCode; +import org.dromara.mica.mqtt.codec.message.MqttConnAckMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttConnAckVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.properties.MqttConnAckProperties; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +import java.util.function.Consumer; + +/** + * MqttConnAckMessage builder + * + * @author netty, L.cm + */ +public final class MqttConnAckBuilder { + private MqttConnectReasonCode returnCode; + private boolean sessionPresent; + private MqttProperties properties = MqttProperties.NO_PROPERTIES; + + public MqttConnAckBuilder() { + } + + public MqttConnAckBuilder returnCode(MqttConnectReasonCode returnCode) { + this.returnCode = returnCode; + return this; + } + + public MqttConnAckBuilder sessionPresent(boolean sessionPresent) { + this.sessionPresent = sessionPresent; + return this; + } + + public MqttConnAckBuilder properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttConnAckBuilder properties(Consumer consumer) { + MqttConnAckProperties connAckProperties = new MqttConnAckProperties(); + consumer.accept(connAckProperties); + return properties(connAckProperties.getProperties()); + } + + public MqttConnAckMessage build() { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.QOS0, false, 0); + MqttConnAckVariableHeader mqttConnAckVariableHeader = + new MqttConnAckVariableHeader(returnCode, sessionPresent, properties); + return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttConnectBuilder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttConnectBuilder.java new file mode 100644 index 0000000..ac4a350 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttConnectBuilder.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.MqttVersion; +import org.dromara.mica.mqtt.codec.message.MqttConnectMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttConnectVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttConnectPayload; +import org.dromara.mica.mqtt.codec.message.properties.MqttConnectProperties; +import org.dromara.mica.mqtt.codec.message.properties.MqttWillPublishProperties; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +import java.util.function.Consumer; + +/** + * MqttConnectMessage builder + * + * @author netty, L.cm + */ +public final class MqttConnectBuilder { + private MqttVersion version = MqttVersion.MQTT_3_1_1; + private String clientId; + private boolean cleanStart; + private boolean hasUser; + private boolean hasPassword; + private int keepAliveSecs; + private MqttProperties willProperties = MqttProperties.NO_PROPERTIES; + private boolean willFlag; + private boolean willRetain; + private MqttQoS willQos = MqttQoS.QOS0; + private String willTopic; + private byte[] willMessage; + private String username; + private byte[] password; + private MqttProperties properties = MqttProperties.NO_PROPERTIES; + + public MqttConnectBuilder() { + } + + public MqttConnectBuilder protocolVersion(MqttVersion version) { + this.version = version; + return this; + } + + public MqttConnectBuilder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public MqttConnectBuilder cleanStart(boolean cleanStart) { + this.cleanStart = cleanStart; + return this; + } + + public MqttConnectBuilder keepAlive(int keepAliveSecs) { + this.keepAliveSecs = keepAliveSecs; + return this; + } + + public MqttConnectBuilder willFlag(boolean willFlag) { + this.willFlag = willFlag; + return this; + } + + public MqttConnectBuilder willQoS(MqttQoS willQos) { + this.willQos = willQos; + return this; + } + + public MqttConnectBuilder willTopic(String willTopic) { + this.willTopic = willTopic; + return this; + } + + public MqttConnectBuilder willMessage(byte[] willMessage) { + this.willMessage = willMessage; + return this; + } + + public MqttConnectBuilder willRetain(boolean willRetain) { + this.willRetain = willRetain; + return this; + } + + public MqttConnectBuilder willProperties(MqttProperties willProperties) { + this.willProperties = willProperties; + return this; + } + + public MqttConnectBuilder willProperties(Consumer consumer) { + MqttWillPublishProperties willPublishProperties = new MqttWillPublishProperties(); + consumer.accept(willPublishProperties); + return willProperties(willPublishProperties.getProperties()); + } + + public MqttConnectBuilder hasUser(boolean value) { + this.hasUser = value; + return this; + } + + public MqttConnectBuilder hasPassword(boolean value) { + this.hasPassword = value; + return this; + } + + public MqttConnectBuilder username(String username) { + this.hasUser = username != null; + this.username = username; + return this; + } + + public MqttConnectBuilder password(byte[] password) { + this.hasPassword = password != null; + this.password = password; + return this; + } + + public MqttConnectBuilder properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttConnectBuilder properties(Consumer consumer) { + MqttConnectProperties connectProperties = new MqttConnectProperties(); + consumer.accept(connectProperties); + return properties(connectProperties.getProperties()); + } + + public MqttConnectMessage build() { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.CONNECT, false, MqttQoS.QOS0, false, 0); + MqttConnectVariableHeader mqttConnectVariableHeader = + new MqttConnectVariableHeader( + version.protocolName(), + version.protocolLevel(), + hasUser, + hasPassword, + willRetain, + willQos.value(), + willFlag, + cleanStart, + keepAliveSecs, + properties); + MqttConnectPayload mqttConnectPayload = + new MqttConnectPayload(clientId, willProperties, willTopic, willMessage, username, password); + return new MqttConnectMessage(mqttFixedHeader, mqttConnectVariableHeader, mqttConnectPayload); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttDisconnectBuilder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttDisconnectBuilder.java new file mode 100644 index 0000000..2edc790 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttDisconnectBuilder.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.codes.MqttDisconnectReasonCode; +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttReasonCodeAndPropertiesVariableHeader; +import org.dromara.mica.mqtt.codec.message.properties.MqttDisconnectProperties; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +import java.util.function.Consumer; + +/** + * MqttDisconnectMessage builder + * @author netty, L.cm + */ +public final class MqttDisconnectBuilder { + private MqttDisconnectReasonCode reasonCode; + private MqttProperties properties = MqttProperties.NO_PROPERTIES; + + public MqttDisconnectBuilder() { + } + + public MqttDisconnectBuilder reasonCode(MqttDisconnectReasonCode reasonCode) { + this.reasonCode = reasonCode; + return this; + } + + public MqttDisconnectBuilder properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttDisconnectBuilder properties(Consumer consumer) { + MqttDisconnectProperties disconnectProperties = new MqttDisconnectProperties(); + consumer.accept(disconnectProperties); + return properties(disconnectProperties.getProperties()); + } + + public MqttMessage build() { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.QOS0, false, 0); + MqttReasonCodeAndPropertiesVariableHeader mqttDisconnectVariableHeader = + new MqttReasonCodeAndPropertiesVariableHeader(reasonCode.value(), properties); + + return new MqttMessage(mqttFixedHeader, mqttDisconnectVariableHeader); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttPubAckBuilder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttPubAckBuilder.java new file mode 100644 index 0000000..8c5b311 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttPubAckBuilder.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.codes.MqttPubAckReasonCode; +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttPubReplyMessageVariableHeader; +import org.dromara.mica.mqtt.codec.message.properties.MqttPubAckProperties; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +import java.util.function.Consumer; + +/** + * MqttPubAckMessage builder + * @author netty, L.cm + */ +public final class MqttPubAckBuilder { + private int packetId; + private MqttPubAckReasonCode reasonCode; + private MqttProperties properties = MqttProperties.NO_PROPERTIES; + + public MqttPubAckBuilder() { + } + + public MqttPubAckBuilder reasonCode(byte reasonCode) { + this.reasonCode = MqttPubAckReasonCode.valueOf(reasonCode); + return this; + } + + public MqttPubAckBuilder reasonCode(MqttPubAckReasonCode reasonCode) { + this.reasonCode = reasonCode; + return this; + } + + public MqttPubAckBuilder packetId(int packetId) { + this.packetId = packetId; + return this; + } + + public MqttPubAckBuilder properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttPubAckBuilder properties(Consumer consumer) { + MqttPubAckProperties pubAckProperties = new MqttPubAckProperties(); + consumer.accept(pubAckProperties); + return properties(pubAckProperties.getProperties()); + } + + public MqttMessage build() { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.QOS0, false, 0); + MqttPubReplyMessageVariableHeader mqttPubAckVariableHeader = + new MqttPubReplyMessageVariableHeader(packetId, reasonCode != null ? reasonCode.value() : 0, properties); + return new MqttMessage(mqttFixedHeader, mqttPubAckVariableHeader); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttPublishBuilder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttPublishBuilder.java new file mode 100644 index 0000000..df8af45 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttPublishBuilder.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttPublishVariableHeader; +import org.dromara.mica.mqtt.codec.message.properties.MqttPublishProperties; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +import java.util.function.Consumer; + +/** + * MqttPublishMessage builder + * + * @author netty, L.cm + */ +public final class MqttPublishBuilder { + private String topic; + private boolean isDup = false; + private boolean retained; + private MqttQoS qos; + private byte[] payload; + private int messageId; + private MqttProperties properties = MqttProperties.NO_PROPERTIES; + + public MqttPublishBuilder() { + } + + public MqttPublishBuilder topicName(String topic) { + this.topic = topic; + return this; + } + + public MqttPublishBuilder isDup(boolean isDup) { + this.isDup = isDup; + return this; + } + + public MqttPublishBuilder retained(boolean retained) { + this.retained = retained; + return this; + } + + public MqttPublishBuilder qos(MqttQoS qos) { + this.qos = qos; + return this; + } + + public MqttPublishBuilder payload(byte[] payload) { + this.payload = payload; + return this; + } + + public MqttPublishBuilder messageId(int messageId) { + this.messageId = messageId; + return this; + } + + public MqttPublishBuilder properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttPublishBuilder properties(Consumer consumer) { + MqttPublishProperties publishProperties = new MqttPublishProperties(); + consumer.accept(publishProperties); + return properties(publishProperties.getProperties()); + } + + public String getTopicName() { + return topic; + } + + public boolean isRetained() { + return retained; + } + + public MqttQoS getQos() { + return qos; + } + + public byte[] getPayload() { + return payload; + } + + public MqttPublishMessage build() { + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, isDup, qos, retained, 0); + MqttPublishVariableHeader mqttVariableHeader = + new MqttPublishVariableHeader(topic, messageId, properties); + return new MqttPublishMessage(mqttFixedHeader, mqttVariableHeader, payload); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubAckBuilder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubAckBuilder.java new file mode 100644 index 0000000..0e56318 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubAckBuilder.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.codes.MqttSubAckReasonCode; +import org.dromara.mica.mqtt.codec.message.MqttSubAckMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdAndPropertiesVariableHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttSubAckPayload; +import org.dromara.mica.mqtt.codec.message.properties.MqttSubAckProperties; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +/** + * MqttSubAckMessage builder + * @author netty, L.cm + */ +public final class MqttSubAckBuilder { + private final List reasonCodes; + private int packetId; + private MqttProperties properties = MqttProperties.NO_PROPERTIES; + + public MqttSubAckBuilder() { + reasonCodes = new ArrayList<>(); + } + + public MqttSubAckBuilder packetId(int packetId) { + this.packetId = packetId; + return this; + } + + public MqttSubAckBuilder properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttSubAckBuilder properties(Consumer consumer) { + MqttSubAckProperties subAckProperties = new MqttSubAckProperties(); + consumer.accept(subAckProperties); + return properties(subAckProperties.getProperties()); + } + + public MqttSubAckBuilder addGrantedQos(MqttQoS qos) { + this.reasonCodes.add(MqttSubAckReasonCode.qosGranted(qos)); + return this; + } + + public MqttSubAckBuilder addReasonCode(MqttSubAckReasonCode reasonCode) { + this.reasonCodes.add(reasonCode); + return this; + } + + public MqttSubAckBuilder addGrantedQoses(MqttQoS... qoses) { + for (MqttQoS qos : qoses) { + this.reasonCodes.add(MqttSubAckReasonCode.qosGranted(qos)); + } + return this; + } + + public MqttSubAckBuilder addReasonCodes(MqttSubAckReasonCode... reasonCodes) { + this.reasonCodes.addAll(Arrays.asList(reasonCodes)); + return this; + } + + public MqttSubAckBuilder addGrantedQosList(List qosList) { + for (MqttQoS qos : qosList) { + this.reasonCodes.add(MqttSubAckReasonCode.qosGranted(qos)); + } + return this; + } + + public MqttSubAckMessage build() { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.QOS0, false, 0); + MqttMessageIdAndPropertiesVariableHeader mqttSubAckVariableHeader = + new MqttMessageIdAndPropertiesVariableHeader(packetId, properties); + // transform to primitive types + short[] grantedQosArray = new short[this.reasonCodes.size()]; + int i = 0; + for (MqttSubAckReasonCode reasonCode : this.reasonCodes) { + grantedQosArray[i++] = reasonCode.value(); + } + MqttSubAckPayload subAckPayload = new MqttSubAckPayload(grantedQosArray); + return new MqttSubAckMessage(mqttFixedHeader, mqttSubAckVariableHeader, subAckPayload); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubscribeBuilder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubscribeBuilder.java new file mode 100644 index 0000000..57cb1e8 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubscribeBuilder.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttSubscribeMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdAndPropertiesVariableHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttSubscribePayload; +import org.dromara.mica.mqtt.codec.message.properties.MqttSubscribeProperties; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +/** + * MqttSubscribeMessage builder + * @author netty, L.cm + */ +public final class MqttSubscribeBuilder { + private final List subscriptions; + private int messageId; + private MqttProperties properties = MqttProperties.NO_PROPERTIES; + + public MqttSubscribeBuilder() { + subscriptions = new ArrayList<>(5); + } + + public MqttSubscribeBuilder addSubscription(MqttTopicSubscription subscription) { + subscriptions.add(subscription); + return this; + } + + public MqttSubscribeBuilder addSubscription(MqttQoS qos, String topic) { + return addSubscription(new MqttTopicSubscription(topic, qos)); + } + + public MqttSubscribeBuilder addSubscription(String topic, MqttSubscriptionOption option) { + return addSubscription(new MqttTopicSubscription(topic, option)); + } + + public MqttSubscribeBuilder addSubscriptions(Collection subscriptionColl) { + subscriptions.addAll(subscriptionColl); + return this; + } + + public MqttSubscribeBuilder messageId(int messageId) { + this.messageId = messageId; + return this; + } + + public MqttSubscribeBuilder properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttSubscribeBuilder properties(Consumer consumer) { + MqttSubscribeProperties subscribeProperties = new MqttSubscribeProperties(); + consumer.accept(subscribeProperties); + return properties(subscribeProperties.getProperties()); + } + + public MqttSubscribeMessage build() { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.SUBSCRIBE, false, MqttQoS.QOS1, false, 0); + MqttMessageIdAndPropertiesVariableHeader mqttVariableHeader = + new MqttMessageIdAndPropertiesVariableHeader(messageId, properties); + MqttSubscribePayload mqttSubscribePayload = new MqttSubscribePayload(subscriptions); + return new MqttSubscribeMessage(mqttFixedHeader, mqttVariableHeader, mqttSubscribePayload); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubscriptionOption.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubscriptionOption.java new file mode 100644 index 0000000..c1461d6 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubscriptionOption.java @@ -0,0 +1,144 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttQoS; + +/** + * Model the SubscriptionOption used in Subscribe MQTT v5 packet + * + * @author netty + */ +public final class MqttSubscriptionOption { + + private final MqttQoS qos; + private final boolean noLocal; + private final boolean retainAsPublished; + private final RetainedHandlingPolicy retainHandling; + public MqttSubscriptionOption(MqttQoS qos, + boolean noLocal, + boolean retainAsPublished, + RetainedHandlingPolicy retainHandling) { + this.qos = qos; + this.noLocal = noLocal; + this.retainAsPublished = retainAsPublished; + this.retainHandling = retainHandling; + } + + public static MqttSubscriptionOption onlyFromQos(MqttQoS qos) { + return new MqttSubscriptionOption(qos, false, false, RetainedHandlingPolicy.SEND_AT_SUBSCRIBE); + } + + public MqttQoS qos() { + return qos; + } + + public boolean isNoLocal() { + return noLocal; + } + + public boolean isRetainAsPublished() { + return retainAsPublished; + } + + public RetainedHandlingPolicy retainHandling() { + return retainHandling; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MqttSubscriptionOption that = (MqttSubscriptionOption) o; + + if (noLocal != that.noLocal) { + return false; + } + if (retainAsPublished != that.retainAsPublished) { + return false; + } + if (qos != that.qos) { + return false; + } + return retainHandling == that.retainHandling; + } + + @Override + public int hashCode() { + int result = qos.hashCode(); + result = 31 * result + (noLocal ? 1 : 0); + result = 31 * result + (retainAsPublished ? 1 : 0); + result = 31 * result + retainHandling.hashCode(); + return result; + } + + @Override + public String toString() { + return "SubscriptionOption[" + + "qos=" + qos + + ", noLocal=" + noLocal + + ", retainAsPublished=" + retainAsPublished + + ", retainHandling=" + retainHandling + + ']'; + } + + /** + * 保留处理政策 + */ + public enum RetainedHandlingPolicy { + /** + * 订阅发送 + */ + SEND_AT_SUBSCRIBE((byte) 0), + /** + * 如果还没有订阅,请发送 + */ + SEND_AT_SUBSCRIBE_IF_NOT_YET_EXISTS((byte) 1), + /** + * 请勿发送订阅 + */ + DONT_SEND_AT_SUBSCRIBE((byte) 2); + + private final byte value; + + RetainedHandlingPolicy(byte value) { + this.value = value; + } + + public static RetainedHandlingPolicy valueOf(int value) { + switch (value) { + case 0: + return SEND_AT_SUBSCRIBE; + case 1: + return SEND_AT_SUBSCRIBE_IF_NOT_YET_EXISTS; + case 2: + return DONT_SEND_AT_SUBSCRIBE; + default: + throw new IllegalArgumentException("invalid RetainedHandlingPolicy: " + value); + } + } + + public byte value() { + return value; + } + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttTopicSubscription.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttTopicSubscription.java new file mode 100644 index 0000000..4649bb3 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttTopicSubscription.java @@ -0,0 +1,77 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.payload.MqttSubscribePayload; + +/** + * Contains a topic name and Qos Level. + * This is part of the {@link MqttSubscribePayload} + * + * @author netty + */ +public final class MqttTopicSubscription { + + private final MqttSubscriptionOption option; + private String topicFilter; + + public MqttTopicSubscription(String topicFilter) { + this(topicFilter, MqttQoS.QOS0); + } + + public MqttTopicSubscription(String topicFilter, MqttQoS qualityOfService) { + this(topicFilter, MqttSubscriptionOption.onlyFromQos(qualityOfService)); + } + + public MqttTopicSubscription(String topicFilter, MqttSubscriptionOption option) { + this.topicFilter = topicFilter; + this.option = option; + } + + public String topicFilter() { + return topicFilter; + } + + /** + * Rewrite topic filter. + * Many IoT devices do not support reconfiguration or upgrade, so it is hard to + * change their subscribed topics. To resolve this issue, MQTT server may offer + * topic rewrite capability. + * + * @param topicFilter Topic to rewrite to + */ + public void setTopicFilter(String topicFilter) { + this.topicFilter = topicFilter; + } + + public MqttQoS qualityOfService() { + return option.qos(); + } + + public MqttSubscriptionOption option() { + return option; + } + + @Override + public String toString() { + return "MqttTopicSubscription[" + + "topicFilter=" + topicFilter + + ", option=" + this.option + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttUnSubAckBuilder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttUnSubAckBuilder.java new file mode 100644 index 0000000..93d805d --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttUnSubAckBuilder.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.codes.MqttUnSubAckReasonCode; +import org.dromara.mica.mqtt.codec.message.MqttUnSubAckMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdAndPropertiesVariableHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttUnsubAckPayload; +import org.dromara.mica.mqtt.codec.message.properties.MqttUnSubAckProperties; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +/** + * MqttUnSubAckMessage builder + * @author netty, L.cm + */ +public final class MqttUnSubAckBuilder { + private final List reasonCodes = new ArrayList<>(); + private int packetId; + private MqttProperties properties = MqttProperties.NO_PROPERTIES; + + public MqttUnSubAckBuilder() { + } + + public MqttUnSubAckBuilder packetId(int packetId) { + this.packetId = packetId; + return this; + } + + public MqttUnSubAckBuilder properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttUnSubAckBuilder properties(Consumer consumer) { + MqttUnSubAckProperties unSubAckProperties = new MqttUnSubAckProperties(); + consumer.accept(unSubAckProperties); + return properties(unSubAckProperties.getProperties()); + } + + public MqttUnSubAckBuilder addReasonCode(short reasonCode) { + this.reasonCodes.add(MqttUnSubAckReasonCode.values()[reasonCode]); + return this; + } + + public MqttUnSubAckBuilder addReasonCode(MqttUnSubAckReasonCode reasonCode) { + this.reasonCodes.add(reasonCode); + return this; + } + + public MqttUnSubAckBuilder addReasonCodes(Short... reasonCodes) { + for (Short reasonCode : reasonCodes) { + this.reasonCodes.add(MqttUnSubAckReasonCode.values()[reasonCode]); + } + return this; + } + + public MqttUnSubAckBuilder addReasonCodes(MqttUnSubAckReasonCode... reasonCodes) { + this.reasonCodes.addAll(Arrays.asList(reasonCodes)); + return this; + } + + public MqttUnSubAckMessage build() { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.UNSUBACK, false, MqttQoS.QOS0, false, 0); + MqttMessageIdAndPropertiesVariableHeader mqttSubAckVariableHeader = + new MqttMessageIdAndPropertiesVariableHeader(packetId, properties); + + List reasonCodeValues = new ArrayList<>(); + for (MqttUnSubAckReasonCode reasonCode : reasonCodes) { + reasonCodeValues.add((short) reasonCode.value()); + } + MqttUnsubAckPayload subAckPayload = new MqttUnsubAckPayload(reasonCodeValues); + return new MqttUnSubAckMessage(mqttFixedHeader, mqttSubAckVariableHeader, subAckPayload); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttUnSubscribeBuilder.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttUnSubscribeBuilder.java new file mode 100644 index 0000000..6f03a64 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttUnSubscribeBuilder.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.builder; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttUnSubscribeMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdAndPropertiesVariableHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttUnsubscribePayload; +import org.dromara.mica.mqtt.codec.message.properties.MqttUnSubscribeProperties; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +/** + * MqttUnSubscribeMessage builder + * @author netty, L.cm + */ +public final class MqttUnSubscribeBuilder { + private final List topicFilters; + private int messageId; + private MqttProperties properties = MqttProperties.NO_PROPERTIES; + + public MqttUnSubscribeBuilder() { + topicFilters = new ArrayList<>(5); + } + + public MqttUnSubscribeBuilder addTopicFilter(String topic) { + topicFilters.add(topic); + return this; + } + + public MqttUnSubscribeBuilder addTopicFilters(Collection topicColl) { + topicFilters.addAll(topicColl); + return this; + } + + public MqttUnSubscribeBuilder messageId(int messageId) { + this.messageId = messageId; + return this; + } + + public MqttUnSubscribeBuilder properties(MqttProperties properties) { + this.properties = properties; + return this; + } + + public MqttUnSubscribeBuilder properties(Consumer consumer) { + MqttUnSubscribeProperties unSubscribeProperties = new MqttUnSubscribeProperties(); + consumer.accept(unSubscribeProperties); + return properties(unSubscribeProperties.getProperties()); + } + + public MqttUnSubscribeMessage build() { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.QOS1, false, 0); + MqttMessageIdAndPropertiesVariableHeader mqttVariableHeader = + new MqttMessageIdAndPropertiesVariableHeader(messageId, properties); + MqttUnsubscribePayload mqttSubscribePayload = new MqttUnsubscribePayload(topicFilters); + return new MqttUnSubscribeMessage(mqttFixedHeader, mqttVariableHeader, mqttSubscribePayload); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttConnAckVariableHeader.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttConnAckVariableHeader.java new file mode 100644 index 0000000..17a2070 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttConnAckVariableHeader.java @@ -0,0 +1,65 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.header; + +import org.dromara.mica.mqtt.codec.codes.MqttConnectReasonCode; +import org.dromara.mica.mqtt.codec.message.MqttConnectMessage; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +/** + * Variable header of {@link MqttConnectMessage} + * + * @author netty + */ +public final class MqttConnAckVariableHeader { + private final MqttConnectReasonCode connectReturnCode; + + private final boolean sessionPresent; + + private final MqttProperties properties; + + public MqttConnAckVariableHeader(MqttConnectReasonCode connectReturnCode, boolean sessionPresent) { + this(connectReturnCode, sessionPresent, MqttProperties.NO_PROPERTIES); + } + + public MqttConnAckVariableHeader(MqttConnectReasonCode connectReturnCode, boolean sessionPresent, + MqttProperties properties) { + this.connectReturnCode = connectReturnCode; + this.sessionPresent = sessionPresent; + this.properties = MqttProperties.withEmptyDefaults(properties); + } + + public MqttConnectReasonCode connectReturnCode() { + return connectReturnCode; + } + + public boolean isSessionPresent() { + return sessionPresent; + } + + public MqttProperties properties() { + return properties; + } + + @Override + public String toString() { + return "MqttConnAckVariableHeader[" + + "connectReturnCode=" + connectReturnCode + + ", sessionPresent=" + sessionPresent + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttConnectVariableHeader.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttConnectVariableHeader.java new file mode 100644 index 0000000..5ee6cf9 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttConnectVariableHeader.java @@ -0,0 +1,138 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.header; + +import org.dromara.mica.mqtt.codec.message.MqttConnectMessage; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +/** + * Variable Header for the {@link MqttConnectMessage} + * + * @author netty + */ +public final class MqttConnectVariableHeader { + + private final String name; + private final int version; + private final boolean hasUsername; + private final boolean hasPassword; + private final boolean isWillRetain; + private final int willQos; + private final boolean isWillFlag; + private final boolean isCleanStart; + private final int keepAliveTimeSeconds; + private final MqttProperties properties; + + public MqttConnectVariableHeader( + String name, + int version, + boolean hasUsername, + boolean hasPassword, + boolean isWillRetain, + int willQos, + boolean isWillFlag, + boolean isCleanStart, + int keepAliveTimeSeconds) { + this(name, + version, + hasUsername, + hasPassword, + isWillRetain, + willQos, + isWillFlag, + isCleanStart, + keepAliveTimeSeconds, + MqttProperties.NO_PROPERTIES); + } + + public MqttConnectVariableHeader( + String name, + int version, + boolean hasUsername, + boolean hasPassword, + boolean isWillRetain, + int willQos, + boolean isWillFlag, + boolean isCleanStart, + int keepAliveTimeSeconds, + MqttProperties properties) { + this.name = name; + this.version = version; + this.hasUsername = hasUsername; + this.hasPassword = hasPassword; + this.isWillRetain = isWillRetain; + this.willQos = willQos; + this.isWillFlag = isWillFlag; + this.isCleanStart = isCleanStart; + this.keepAliveTimeSeconds = keepAliveTimeSeconds; + this.properties = MqttProperties.withEmptyDefaults(properties); + } + + public String name() { + return name; + } + + public int version() { + return version; + } + + public boolean hasUsername() { + return hasUsername; + } + + public boolean hasPassword() { + return hasPassword; + } + + public boolean isWillRetain() { + return isWillRetain; + } + + public int willQos() { + return willQos; + } + + public boolean isWillFlag() { + return isWillFlag; + } + + public boolean isCleanStart() { + return isCleanStart; + } + + public int keepAliveTimeSeconds() { + return keepAliveTimeSeconds; + } + + public MqttProperties properties() { + return properties; + } + + @Override + public String toString() { + return "MqttConnectVariableHeader[" + + "name=" + name + + ", version=" + version + + ", hasUsername=" + hasUsername + + ", hasPassword=" + hasPassword + + ", isWillRetain=" + isWillRetain + + ", isWillFlag=" + isWillFlag + + ", isCleanStart=" + isCleanStart + + ", keepAliveTimeSeconds=" + keepAliveTimeSeconds + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttFixedHeader.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttFixedHeader.java new file mode 100644 index 0000000..7be82f0 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttFixedHeader.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.header; + +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; + +import java.util.Objects; + +/** + * See + * MQTTV3.1/fixed-header + * + * @author netty、L.cm + */ +public final class MqttFixedHeader { + + private final MqttMessageType messageType; + private final boolean isDup; + private final boolean isRetain; + private final int headLength; + private final int remainingLength; + private MqttQoS qosLevel; + + public MqttFixedHeader( + MqttMessageType messageType, + boolean isDup, + MqttQoS qosLevel, + boolean isRetain, + int remainingLength) { + this(messageType, isDup, qosLevel, isRetain, 0, remainingLength); + } + + public MqttFixedHeader( + MqttMessageType messageType, + boolean isDup, + MqttQoS qosLevel, + boolean isRetain, + int headLength, + int remainingLength) { + this.messageType = Objects.requireNonNull(messageType, "messageType is null."); + this.isDup = isDup; + this.qosLevel = Objects.requireNonNull(qosLevel, "qosLevel is null."); + this.isRetain = isRetain; + this.headLength = headLength; + this.remainingLength = remainingLength; + } + + public MqttMessageType messageType() { + return messageType; + } + + public boolean isDup() { + return isDup; + } + + public MqttQoS qosLevel() { + return qosLevel; + } + + /** + * 做 qos 降级,mqtt 规定 qos 大于 0,messageId 必须大于 0,为了兼容,固做降级处理 + */ + public void downgradeQos() { + this.qosLevel = MqttQoS.QOS0; + } + + public boolean isRetain() { + return isRetain; + } + + public int headLength() { + return headLength; + } + + public int remainingLength() { + return remainingLength; + } + + public int getMessageLength() { + return headLength + remainingLength; + } + + @Override + public String toString() { + return "MqttFixedHeader[" + + "messageType=" + messageType + + ", isDup=" + isDup + + ", qosLevel=" + qosLevel + + ", isRetain=" + isRetain + + ", remainingLength=" + remainingLength + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttMessageIdAndPropertiesVariableHeader.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttMessageIdAndPropertiesVariableHeader.java new file mode 100644 index 0000000..0544863 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttMessageIdAndPropertiesVariableHeader.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.header; + +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +/** + * Variable Header containing, Packet Id and Properties as in MQTT v5 spec. + * + * @author netty + */ +public final class MqttMessageIdAndPropertiesVariableHeader extends MqttMessageIdVariableHeader { + private final MqttProperties properties; + + public MqttMessageIdAndPropertiesVariableHeader(int messageId, MqttProperties properties) { + super(messageId); + if (messageId < 1 || messageId > 0xffff) { + throw new IllegalArgumentException("messageId: " + messageId + " (expected: 1 ~ 65535)"); + } + this.properties = MqttProperties.withEmptyDefaults(properties); + } + + public MqttProperties properties() { + return properties; + } + + @Override + public String toString() { + return "MqttMessageIdAndPropertiesVariableHeader[" + + "messageId=" + messageId() + + ", properties=" + properties + + ']'; + } + + @Override + public MqttMessageIdAndPropertiesVariableHeader withDefaultEmptyProperties() { + return this; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttMessageIdVariableHeader.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttMessageIdVariableHeader.java new file mode 100644 index 0000000..b6fbf1d --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttMessageIdVariableHeader.java @@ -0,0 +1,59 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.header; + +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +/** + * Variable Header containing only Message Id + * See MQTTV3.1/msg-id + * + * @author netty + */ +public class MqttMessageIdVariableHeader { + private final int messageId; + + protected MqttMessageIdVariableHeader(int messageId) { + this.messageId = messageId; + } + + public static MqttMessageIdVariableHeader from(int messageId) { + if (messageId < 1 || messageId > 0xffff) { + throw new IllegalArgumentException("messageId: " + messageId + " (expected: 1 ~ 65535)"); + } + return new MqttMessageIdVariableHeader(messageId); + } + + public int messageId() { + return messageId; + } + + @Override + public String toString() { + return "MqttMessageIdVariableHeader[" + + "messageId=" + messageId + + ']'; + } + + public MqttMessageIdAndPropertiesVariableHeader withEmptyProperties() { + return new MqttMessageIdAndPropertiesVariableHeader(messageId, MqttProperties.NO_PROPERTIES); + } + + public MqttMessageIdAndPropertiesVariableHeader withDefaultEmptyProperties() { + return withEmptyProperties(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttPubReplyMessageVariableHeader.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttPubReplyMessageVariableHeader.java new file mode 100644 index 0000000..40096ae --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttPubReplyMessageVariableHeader.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.header; + +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +/** + * Variable Header containing Packet Id, reason code and Properties as in MQTT v5 spec. + * + * @author netty + */ +public final class MqttPubReplyMessageVariableHeader extends MqttMessageIdVariableHeader { + public static final byte REASON_CODE_OK = 0; + private final byte reasonCode; + private final MqttProperties properties; + + public MqttPubReplyMessageVariableHeader(int messageId, byte reasonCode, MqttProperties properties) { + super(messageId); + if (messageId < 1 || messageId > 0xffff) { + throw new IllegalArgumentException("messageId: " + messageId + " (expected: 1 ~ 65535)"); + } + this.reasonCode = reasonCode; + this.properties = MqttProperties.withEmptyDefaults(properties); + } + + public byte reasonCode() { + return reasonCode; + } + + public MqttProperties properties() { + return properties; + } + + @Override + public String toString() { + return "MqttPubReplyMessageVariableHeader[" + + "messageId=" + messageId() + + ", reasonCode=" + reasonCode + + ", properties=" + properties + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttPublishVariableHeader.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttPublishVariableHeader.java new file mode 100644 index 0000000..f4d5cc4 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttPublishVariableHeader.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.header; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +/** + * Variable Header of the {@link MqttPublishMessage} + * + * @author netty + */ +public final class MqttPublishVariableHeader { + private final String topicName; + private final int packetId; + private final MqttProperties properties; + + public MqttPublishVariableHeader(String topicName, int packetId) { + this(topicName, packetId, MqttProperties.NO_PROPERTIES); + } + + public MqttPublishVariableHeader(String topicName, int packetId, MqttProperties properties) { + this.topicName = topicName; + this.packetId = packetId; + this.properties = MqttProperties.withEmptyDefaults(properties); + } + + public String topicName() { + return topicName; + } + + public int packetId() { + return packetId; + } + + public MqttProperties properties() { + return properties; + } + + @Override + public String toString() { + return "MqttPublishVariableHeader[" + + "topicName=" + topicName + + ", packetId=" + packetId + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttReasonCodeAndPropertiesVariableHeader.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttReasonCodeAndPropertiesVariableHeader.java new file mode 100644 index 0000000..23ddfb2 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttReasonCodeAndPropertiesVariableHeader.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.header; + +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +/** + * Variable Header for AUTH and DISCONNECT messages represented by {@link MqttMessage} + * + * @author netty + */ +public final class MqttReasonCodeAndPropertiesVariableHeader { + public static final byte REASON_CODE_OK = 0; + private final byte reasonCode; + private final MqttProperties properties; + + public MqttReasonCodeAndPropertiesVariableHeader(byte reasonCode, + MqttProperties properties) { + this.reasonCode = reasonCode; + this.properties = MqttProperties.withEmptyDefaults(properties); + } + + public byte reasonCode() { + return reasonCode; + } + + public MqttProperties properties() { + return properties; + } + + @Override + public String toString() { + return "MqttReasonCodeAndPropertiesVariableHeader[" + + "reasonCode=" + reasonCode + + ", properties=" + properties + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttConnectPayload.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttConnectPayload.java new file mode 100644 index 0000000..539c02a --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttConnectPayload.java @@ -0,0 +1,105 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.payload; + +import org.dromara.mica.mqtt.codec.message.MqttConnectMessage; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Payload of {@link MqttConnectMessage} + * + * @author netty + */ +public final class MqttConnectPayload { + private final String clientIdentifier; + private final MqttProperties willProperties; + private final String willTopic; + private final byte[] willMessage; + private final String username; + private final byte[] password; + + public MqttConnectPayload( + String clientIdentifier, + String willTopic, + byte[] willMessage, + String username, + byte[] password) { + this(clientIdentifier, + MqttProperties.NO_PROPERTIES, + willTopic, + willMessage, + username, + password); + } + + public MqttConnectPayload( + String clientIdentifier, + MqttProperties willProperties, + String willTopic, + byte[] willMessage, + String username, + byte[] password) { + this.clientIdentifier = clientIdentifier; + this.willProperties = MqttProperties.withEmptyDefaults(willProperties); + this.willTopic = willTopic; + this.willMessage = willMessage; + this.username = username; + this.password = password; + } + + public String clientIdentifier() { + return clientIdentifier; + } + + public MqttProperties willProperties() { + return willProperties; + } + + public String willTopic() { + return willTopic; + } + + public byte[] willMessageInBytes() { + return willMessage; + } + + public String username() { + return username; + } + + public byte[] passwordInBytes() { + return password; + } + + public String password() { + return password == null ? null : new String(password, StandardCharsets.UTF_8); + } + + @Override + public String toString() { + return "MqttConnectPayload[" + + "clientIdentifier=" + clientIdentifier + + ", willTopic=" + willTopic + + ", willMessage=" + Arrays.toString(willMessage) + + ", username=" + username + + ", password=" + Arrays.toString(password) + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttSubAckPayload.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttSubAckPayload.java new file mode 100644 index 0000000..673236a --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttSubAckPayload.java @@ -0,0 +1,78 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.payload; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttSubAckMessage; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Payload of the {@link MqttSubAckMessage} + * + * @author netty + */ +public class MqttSubAckPayload { + private final List reasonCodes; + + public MqttSubAckPayload(short[] reasonCodes) { + Objects.requireNonNull(reasonCodes, "reasonCodes is null."); + List list = new ArrayList<>(reasonCodes.length); + for (short v : reasonCodes) { + list.add(v); + } + this.reasonCodes = Collections.unmodifiableList(list); + } + + public MqttSubAckPayload(Iterable reasonCodes) { + Objects.requireNonNull(reasonCodes, "reasonCodes is null."); + List list = new ArrayList<>(); + for (Short v : reasonCodes) { + if (v == null) { + break; + } + list.add(v); + } + this.reasonCodes = Collections.unmodifiableList(list); + } + + public List grantedQoSLevels() { + List qosLevels = new ArrayList<>(reasonCodes.size()); + for (Short code : reasonCodes) { + if (code > MqttQoS.QOS2.value()) { + qosLevels.add(MqttQoS.FAILURE.value()); + } else { + qosLevels.add(code); + } + } + return qosLevels; + } + + public List reasonCodes() { + return reasonCodes; + } + + @Override + public String toString() { + return "MqttSubAckPayload[" + + "reasonCodes=" + reasonCodes + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttSubscribePayload.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttSubscribePayload.java new file mode 100644 index 0000000..07fb442 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttSubscribePayload.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.payload; + +import org.dromara.mica.mqtt.codec.message.MqttSubscribeMessage; +import org.dromara.mica.mqtt.codec.message.builder.MqttTopicSubscription; + +import java.util.Collections; +import java.util.List; + +/** + * Payload of the {@link MqttSubscribeMessage} + * + * @author netty + */ +public final class MqttSubscribePayload { + + private final List topicSubscriptions; + + public MqttSubscribePayload(List topicSubscriptions) { + this.topicSubscriptions = Collections.unmodifiableList(topicSubscriptions); + } + + public List topicSubscriptions() { + return topicSubscriptions; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("MqttSubscribePayload["); + for (MqttTopicSubscription topicSubscription : topicSubscriptions) { + builder.append(topicSubscription).append(", "); + } + if (!topicSubscriptions.isEmpty()) { + builder.setLength(builder.length() - 2); + } + return builder.append(']').toString(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttUnsubAckPayload.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttUnsubAckPayload.java new file mode 100644 index 0000000..c6e8130 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttUnsubAckPayload.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.payload; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Payload for MQTT unsuback message as in V5. + * + * @author netty + */ +public final class MqttUnsubAckPayload { + + private static final MqttUnsubAckPayload EMPTY = new MqttUnsubAckPayload(); + private final List unsubscribeReasonCodes; + + public MqttUnsubAckPayload() { + this.unsubscribeReasonCodes = Collections.emptyList(); + } + + public MqttUnsubAckPayload(short[] unsubscribeReasonCodes) { + Objects.requireNonNull(unsubscribeReasonCodes, "unsubscribeReasonCodes is null."); + List list = new ArrayList<>(); + for (short v : unsubscribeReasonCodes) { + list.add(v); + } + this.unsubscribeReasonCodes = Collections.unmodifiableList(list); + } + + public MqttUnsubAckPayload(Iterable unsubscribeReasonCodes) { + Objects.requireNonNull(unsubscribeReasonCodes, "unsubscribeReasonCodes is null."); + List list = new ArrayList<>(); + for (Short v : unsubscribeReasonCodes) { + Objects.requireNonNull(v, "unsubscribeReasonCode is null."); + list.add(v); + } + this.unsubscribeReasonCodes = Collections.unmodifiableList(list); + } + + public static MqttUnsubAckPayload withEmptyDefaults(MqttUnsubAckPayload payload) { + if (payload == null) { + return EMPTY; + } else { + return payload; + } + } + + public List unsubscribeReasonCodes() { + return unsubscribeReasonCodes; + } + + @Override + public String toString() { + return "MqttUnsubAckPayload[" + + "unsubscribeReasonCodes=" + unsubscribeReasonCodes + + ']'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttUnsubscribePayload.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttUnsubscribePayload.java new file mode 100644 index 0000000..d354a9b --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttUnsubscribePayload.java @@ -0,0 +1,52 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.message.payload; + +import org.dromara.mica.mqtt.codec.message.MqttUnSubscribeMessage; + +import java.util.Collections; +import java.util.List; + +/** + * Payload of the {@link MqttUnSubscribeMessage} + * + * @author netty + */ +public final class MqttUnsubscribePayload { + private final List topics; + + public MqttUnsubscribePayload(List topics) { + this.topics = Collections.unmodifiableList(topics); + } + + public List topics() { + return topics; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("MqttUnsubscribePayload["); + for (String topic : topics) { + builder.append("topicName = ").append(topic).append(", "); + } + if (!topics.isEmpty()) { + builder.setLength(builder.length() - 2); + } + return builder.append(']').toString(); + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttAuthProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttAuthProperties.java new file mode 100644 index 0000000..53156a4 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttAuthProperties.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.*; + +/** + * mqtt5 认证属性 + * + * @author L.cm + */ +public class MqttAuthProperties { + private final MqttProperties properties; + + public MqttAuthProperties() { + this(new MqttProperties()); + } + + public MqttAuthProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置认证方法 + * + * @param authenticationMethod 认证方法 + * @return MqttAuthProperty + */ + public MqttAuthProperties setAuthenticationMethod(String authenticationMethod) { + properties.add(new StringProperty(MqttPropertyType.AUTHENTICATION_METHOD, authenticationMethod)); + return this; + } + + /** + * 设置认证数据 + * + * @param authenticationData 认证数据 + * @return MqttAuthProperty + */ + public MqttAuthProperties setAuthenticationData(byte[] authenticationData) { + properties.add(new BinaryProperty(MqttPropertyType.AUTHENTICATION_DATA, authenticationData)); + return this; + } + + /** + * 设置原因字符串 + * + * @param reasonString 原因字符串 + * @return MqttAuthProperty + */ + public MqttAuthProperties setReasonString(String reasonString) { + properties.add(new StringProperty(MqttPropertyType.REASON_STRING, reasonString)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttAuthProperty + */ + public MqttAuthProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttAuthProperty + */ + public MqttAuthProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttConnAckProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttConnAckProperties.java new file mode 100644 index 0000000..3ae7df8 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttConnAckProperties.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.*; + +/** + * MQTT5 CONNACK 属性 + * + * @author L.cm + */ +public class MqttConnAckProperties { + private final MqttProperties properties; + + public MqttConnAckProperties() { + this(new MqttProperties()); + } + + public MqttConnAckProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置会话过期间隔 + * + * @param sessionExpiryInterval 会话过期间隔 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setSessionExpiryInterval(int sessionExpiryInterval) { + properties.add(new IntegerProperty(MqttPropertyType.SESSION_EXPIRY_INTERVAL, sessionExpiryInterval)); + return this; + } + + /** + * 设置分配的客户端标识符 + * + * @param assignedClientIdentifier 客户端标识符 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setAssignedClientIdentifier(String assignedClientIdentifier) { + properties.add(new StringProperty(MqttPropertyType.ASSIGNED_CLIENT_IDENTIFIER, assignedClientIdentifier)); + return this; + } + + /** + * 设置服务器保持连接时间 + * + * @param serverKeepAlive 保持连接时间 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setServerKeepAlive(int serverKeepAlive) { + properties.add(new IntegerProperty(MqttPropertyType.SERVER_KEEP_ALIVE, serverKeepAlive)); + return this; + } + + /** + * 设置认证方法 + * + * @param authenticationMethod 认证方法 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setAuthenticationMethod(String authenticationMethod) { + properties.add(new StringProperty(MqttPropertyType.AUTHENTICATION_METHOD, authenticationMethod)); + return this; + } + + /** + * 设置认证数据 + * + * @param authenticationData 认证数据 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setAuthenticationData(byte[] authenticationData) { + properties.add(new BinaryProperty(MqttPropertyType.AUTHENTICATION_DATA, authenticationData)); + return this; + } + + /** + * 设置响应信息 + * + * @param responseInformation 响应信息 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setResponseInformation(String responseInformation) { + properties.add(new StringProperty(MqttPropertyType.RESPONSE_INFORMATION, responseInformation)); + return this; + } + + /** + * 设置服务器引用 + * + * @param serverReference 服务器引用 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setServerReference(String serverReference) { + properties.add(new StringProperty(MqttPropertyType.SERVER_REFERENCE, serverReference)); + return this; + } + + /** + * 设置接收最大数量 + * + * @param receiveMaximum 接收最大数量 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setReceiveMaximum(int receiveMaximum) { + properties.add(new IntegerProperty(MqttPropertyType.RECEIVE_MAXIMUM, receiveMaximum)); + return this; + } + + /** + * 设置主题别名最大值 + * + * @param topicAliasMaximum 主题别名最大值 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setTopicAliasMaximum(int topicAliasMaximum) { + properties.add(new IntegerProperty(MqttPropertyType.TOPIC_ALIAS_MAXIMUM, topicAliasMaximum)); + return this; + } + + /** + * 设置最大QOS + * + * @param maximumQos 最大QOS + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setMaximumQos(int maximumQos) { + properties.add(new IntegerProperty(MqttPropertyType.MAXIMUM_QOS, maximumQos)); + return this; + } + + /** + * 设置保留可用标志 + * + * @param retainAvailable 是否保留可用 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setRetainAvailable(boolean retainAvailable) { + properties.add(new BooleanProperty(MqttPropertyType.RETAIN_AVAILABLE, retainAvailable)); + return this; + } + + /** + * 设置最大数据包大小 + * + * @param maximumPacketSize 最大数据包大小 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setMaximumPacketSize(int maximumPacketSize) { + properties.add(new IntegerProperty(MqttPropertyType.MAXIMUM_PACKET_SIZE, maximumPacketSize)); + return this; + } + + /** + * 设置通配符订阅可用标志 + * + * @param wildcardSubscriptionAvailable 是否通配符订阅可用 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setWildcardSubscriptionAvailable(boolean wildcardSubscriptionAvailable) { + properties.add(new BooleanProperty(MqttPropertyType.WILDCARD_SUBSCRIPTION_AVAILABLE, wildcardSubscriptionAvailable)); + return this; + } + + /** + * 设置订阅标识符可用标志 + * + * @param subscriptionIdentifiersAvailable 是否订阅标识符可用 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setSubscriptionIdentifiersAvailable(boolean subscriptionIdentifiersAvailable) { + properties.add(new BooleanProperty(MqttPropertyType.SUBSCRIPTION_IDENTIFIER_AVAILABLE, subscriptionIdentifiersAvailable)); + return this; + } + + /** + * 设置共享订阅可用标志 + * + * @param sharedSubscriptionAvailable 是否共享订阅可用 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setSharedSubscriptionAvailable(boolean sharedSubscriptionAvailable) { + properties.add(new BooleanProperty(MqttPropertyType.SHARED_SUBSCRIPTION_AVAILABLE, sharedSubscriptionAvailable)); + return this; + } + + /** + * 设置原因字符串 + * + * @param reasonString 原因字符串 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties setReasonString(String reasonString) { + properties.add(new StringProperty(MqttPropertyType.REASON_STRING, reasonString)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttConnAckProperty + */ + public MqttConnAckProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttConnAckProperty + */ + public MqttConnAckProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttConnectProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttConnectProperties.java new file mode 100644 index 0000000..817c7a4 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttConnectProperties.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.*; + +/** + * mqtt5 连接属性 + * + * @author L.cm + */ +public class MqttConnectProperties { + private final MqttProperties properties; + + public MqttConnectProperties() { + this(new MqttProperties()); + } + + public MqttConnectProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置会话过期时间 + * + * @param sessionExpiryInterval 会话过期时间 + * @return MqttConnectProperty + */ + public MqttConnectProperties setSessionExpiryInterval(int sessionExpiryInterval) { + properties.add(new IntegerProperty(MqttPropertyType.SESSION_EXPIRY_INTERVAL, sessionExpiryInterval)); + return this; + } + + /** + * 设置认证方法 + * + * @param authenticationMethod 认证方法 + * @return MqttConnectProperty + */ + public MqttConnectProperties setAuthenticationMethod(String authenticationMethod) { + properties.add(new StringProperty(MqttPropertyType.AUTHENTICATION_METHOD, authenticationMethod)); + return this; + } + + /** + * 设置认证数据 + * + * @param authenticationData 认证数据 + * @return MqttConnectProperty + */ + public MqttConnectProperties setAuthenticationData(byte[] authenticationData) { + properties.add(new BinaryProperty(MqttPropertyType.AUTHENTICATION_DATA, authenticationData)); + return this; + } + + /** + * 设置请求问题信息 + * + * @param requestProblemInformation 请求问题信息 + * @return MqttConnectProperty + */ + public MqttConnectProperties setRequestProblemInformation(boolean requestProblemInformation) { + properties.add(new BooleanProperty(MqttPropertyType.REQUEST_PROBLEM_INFORMATION, requestProblemInformation)); + return this; + } + + /** + * 设置请求响应信息 + * + * @param requestResponseInformation 请求响应信息 + * @return MqttConnectProperty + */ + public MqttConnectProperties setRequestResponseInformation(boolean requestResponseInformation) { + properties.add(new BooleanProperty(MqttPropertyType.REQUEST_RESPONSE_INFORMATION, requestResponseInformation)); + return this; + } + + /** + * 设置接收最大包数 + * + * @param receiveMaximum 接收最大包数 + * @return MqttConnectProperty + */ + public MqttConnectProperties setReceiveMaximum(int receiveMaximum) { + properties.add(new IntegerProperty(MqttPropertyType.RECEIVE_MAXIMUM, receiveMaximum)); + return this; + } + + /** + * 设置主题别名最大数 + * + * @param topicAliasMaximum 主题别名最大数 + * @return MqttConnectProperty + */ + public MqttConnectProperties setTopicAliasMaximum(int topicAliasMaximum) { + properties.add(new IntegerProperty(MqttPropertyType.TOPIC_ALIAS_MAXIMUM, topicAliasMaximum)); + return this; + } + + /** + * 设置最大包大小 + * + * @param maximumPacketSize 最大包大小 + * @return MqttConnectProperty + */ + public MqttConnectProperties setMaximumPacketSize(int maximumPacketSize) { + properties.add(new IntegerProperty(MqttPropertyType.MAXIMUM_PACKET_SIZE, maximumPacketSize)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttConnectProperty + */ + public MqttConnectProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttConnectProperty + */ + public MqttConnectProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttDisconnectProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttDisconnectProperties.java new file mode 100644 index 0000000..92ab2d6 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttDisconnectProperties.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.*; + +/** + * MQTT5 DISCONNECT 属性类,用于存储断开连接相关的属性信息 + * + * @author L.cm + */ +public class MqttDisconnectProperties { + private final MqttProperties properties; + + public MqttDisconnectProperties() { + this(new MqttProperties()); + } + + public MqttDisconnectProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置会话过期间隔 + * + * @param interval 会话过期间隔 + * @return MqttDisconnectProperty + */ + public MqttDisconnectProperties setSessionExpiryInterval(int interval) { + properties.add(new IntegerProperty(MqttPropertyType.SESSION_EXPIRY_INTERVAL, interval)); + return this; + } + + /** + * 设置服务器引用 + * + * @param serverReference 服务器引用 + * @return MqttDisconnectProperty + */ + public MqttDisconnectProperties setServerReference(String serverReference) { + properties.add(new StringProperty(MqttPropertyType.SERVER_REFERENCE, serverReference)); + return this; + } + + /** + * 设置原因字符串 + * + * @param reasonString 原因字符串 + * @return MqttDisconnectProperty + */ + public MqttDisconnectProperties setReasonString(String reasonString) { + properties.add(new StringProperty(MqttPropertyType.REASON_STRING, reasonString)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttDisconnectProperty + */ + public MqttDisconnectProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttDisconnectProperty + */ + public MqttDisconnectProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubAckProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubAckProperties.java new file mode 100644 index 0000000..7acd3a8 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubAckProperties.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.properties.MqttPropertyType; +import org.dromara.mica.mqtt.codec.properties.StringProperty; +import org.dromara.mica.mqtt.codec.properties.UserProperty; + +/** + * mqtt5 发布 ack 属性 + * + * @author L.cm + */ +public class MqttPubAckProperties { + private final MqttProperties properties; + + public MqttPubAckProperties() { + this(new MqttProperties()); + } + + public MqttPubAckProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置原因字符串 + * + * @param reasonString 原因字符串 + * @return MqttPubAckProperties + */ + public MqttPubAckProperties setReasonString(String reasonString) { + properties.add(new StringProperty(MqttPropertyType.REASON_STRING, reasonString)); + return this; + } + + /** + * 添加用户属性 + * + * @param userProperty 用户属性 + * @return MqttPubAckProperty + */ + public MqttPubAckProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttPubAckProperty + */ + public MqttPubAckProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubCompProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubCompProperties.java new file mode 100644 index 0000000..cbdfc55 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubCompProperties.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.properties.MqttPropertyType; +import org.dromara.mica.mqtt.codec.properties.StringProperty; +import org.dromara.mica.mqtt.codec.properties.UserProperty; + +/** + * mqtt5 发布完成属性 + * + * @author L.cm + */ +public class MqttPubCompProperties { + private final MqttProperties properties; + + public MqttPubCompProperties() { + this(new MqttProperties()); + } + + public MqttPubCompProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置原因字符串 + * + * @param reasonString 原因字符串 + * @return MqttPubCompProperties + */ + public MqttPubCompProperties setReasonString(String reasonString) { + properties.add(new StringProperty(MqttPropertyType.REASON_STRING, reasonString)); + return this; + } + + /** + * 添加用户属性 + * + * @param userProperty 用户属性 + * @return MqttPubCompProperty + */ + public MqttPubCompProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttPubCompProperty + */ + public MqttPubCompProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubRecProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubRecProperties.java new file mode 100644 index 0000000..c824ef9 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubRecProperties.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.properties.MqttPropertyType; +import org.dromara.mica.mqtt.codec.properties.StringProperty; +import org.dromara.mica.mqtt.codec.properties.UserProperty; + +/** + * mqtt5 发布接收属性 + * + * @author L.cm + */ +public class MqttPubRecProperties { + private final MqttProperties properties; + + public MqttPubRecProperties() { + this(new MqttProperties()); + } + + public MqttPubRecProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置原因字符串 + * + * @param reasonString 原因字符串 + * @return MqttPubRecProperties + */ + public MqttPubRecProperties setReasonString(String reasonString) { + properties.add(new StringProperty(MqttPropertyType.REASON_STRING, reasonString)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttPubRecProperty + */ + public MqttPubRecProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttPubRecProperty + */ + public MqttPubRecProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubRelProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubRelProperties.java new file mode 100644 index 0000000..936c232 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubRelProperties.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.properties.MqttPropertyType; +import org.dromara.mica.mqtt.codec.properties.StringProperty; +import org.dromara.mica.mqtt.codec.properties.UserProperty; + +/** + * mqtt5 发布确认属性 + * + * @author L.cm + */ +public class MqttPubRelProperties { + private final MqttProperties properties; + + public MqttPubRelProperties() { + this(new MqttProperties()); + } + + public MqttPubRelProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置原因字符串 + * + * @param reasonString 原因字符串 + * @return MqttPubRelProperties + */ + public MqttPubRelProperties setReasonString(String reasonString) { + properties.add(new StringProperty(MqttPropertyType.REASON_STRING, reasonString)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttPubRelProperties + */ + public MqttPubRelProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttPubRelProperties + */ + public MqttPubRelProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPublishProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPublishProperties.java new file mode 100644 index 0000000..4c77ddc --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPublishProperties.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.*; + +/** + * MQTT5 消息发布属性类,用于存储发布消息相关的属性信息 + * + * @author L.cm + */ +public class MqttPublishProperties { + private final MqttProperties properties; + + public MqttPublishProperties() { + this(new MqttProperties()); + } + + public MqttPublishProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置负载格式指示器 + * + * @param indicator 负载格式指示器 (0 表示未指定, 1 表示 UTF-8 编码) + * @return MqttPublishProperties + */ + public MqttPublishProperties setPayloadFormatIndicator(int indicator) { + properties.add(new IntegerProperty(MqttPropertyType.PAYLOAD_FORMAT_INDICATOR, indicator)); + return this; + } + + /** + * 设置消息过期时间间隔 + * + * @param interval 消息过期时间间隔(秒) + * @return MqttPublishProperty + */ + public MqttPublishProperties setMessageExpiryInterval(int interval) { + properties.add(new IntegerProperty(MqttPropertyType.MESSAGE_EXPIRY_INTERVAL, interval)); + return this; + } + + /** + * 设置关联数据 + * + * @param correlationData 关联数据 + * @return MqttPublishProperty + */ + public MqttPublishProperties setCorrelationData(byte[] correlationData) { + properties.add(new BinaryProperty(MqttPropertyType.CORRELATION_DATA, correlationData)); + return this; + } + + /** + * 设置内容类型 + * + * @param contentType 内容类型 + * @return MqttPublishProperty + */ + public MqttPublishProperties setContentType(String contentType) { + properties.add(new StringProperty(MqttPropertyType.CONTENT_TYPE, contentType)); + return this; + } + + /** + * 设置响应主题 + * + * @param responseTopic 响应主题 + * @return MqttPublishProperty + */ + public MqttPublishProperties setResponseTopic(String responseTopic) { + properties.add(new StringProperty(MqttPropertyType.RESPONSE_TOPIC, responseTopic)); + return this; + } + + /** + * 设置订阅标识符 + * + * @param subscriptionIdentifier 订阅标识符 + * @return MqttPublishProperty + */ + public MqttPublishProperties setSubscriptionIdentifier(int subscriptionIdentifier) { + properties.add(new IntegerProperty(MqttPropertyType.SUBSCRIPTION_IDENTIFIER, subscriptionIdentifier)); + return this; + } + + /** + * 设置主题别名 + * + * @param topicAlias 主题别名 + * @return MqttPublishProperty + */ + public MqttPublishProperties setTopicAlias(int topicAlias) { + properties.add(new IntegerProperty(MqttPropertyType.TOPIC_ALIAS, topicAlias)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttPublishProperty + */ + public MqttPublishProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttPublishProperty + */ + public MqttPublishProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttSubAckProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttSubAckProperties.java new file mode 100644 index 0000000..4e23df5 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttSubAckProperties.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.properties.MqttPropertyType; +import org.dromara.mica.mqtt.codec.properties.StringProperty; +import org.dromara.mica.mqtt.codec.properties.UserProperty; + +/** + * MQTT5 SUBACK 消息属性 + * + * @author L.cm + */ +public class MqttSubAckProperties { + private final MqttProperties properties; + + public MqttSubAckProperties() { + this(new MqttProperties()); + } + + public MqttSubAckProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置原因字符串 + * + * @param reasonString 原因字符串 + * @return MqttSubAckProperties + */ + public MqttSubAckProperties setReasonString(String reasonString) { + properties.add(new StringProperty(MqttPropertyType.REASON_STRING, reasonString)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttSubAckProperty + */ + public MqttSubAckProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttSubAckProperty + */ + public MqttSubAckProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttSubscribeProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttSubscribeProperties.java new file mode 100644 index 0000000..e501181 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttSubscribeProperties.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.IntegerProperty; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.properties.MqttPropertyType; +import org.dromara.mica.mqtt.codec.properties.UserProperty; + +/** + * mqtt5 订阅确认属性 + * + * @author L.cm + */ +public class MqttSubscribeProperties { + private final MqttProperties properties; + + public MqttSubscribeProperties() { + this(new MqttProperties()); + } + + public MqttSubscribeProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置订阅标识符 + * + * @param subscriptionIdentifier 订阅标识符 + * @return MqttSubscribeProperties + */ + public MqttSubscribeProperties setSubscriptionIdentifier(int subscriptionIdentifier) { + properties.add(new IntegerProperty(MqttPropertyType.SUBSCRIPTION_IDENTIFIER, subscriptionIdentifier)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttSubscribeProperty + */ + public MqttSubscribeProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttSubscribeProperty + */ + public MqttSubscribeProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttUnSubAckProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttUnSubAckProperties.java new file mode 100644 index 0000000..aa19bc7 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttUnSubAckProperties.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.properties.MqttPropertyType; +import org.dromara.mica.mqtt.codec.properties.StringProperty; +import org.dromara.mica.mqtt.codec.properties.UserProperty; + +/** + * mqtt5 取消订阅确认属性 + * + * @author L.cm + */ +public class MqttUnSubAckProperties { + private final MqttProperties properties; + + public MqttUnSubAckProperties() { + this(new MqttProperties()); + } + + public MqttUnSubAckProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置原因字符串 + * + * @param reasonString 原因字符串 + * @return MqttUnSubAckProperties + */ + public MqttUnSubAckProperties setReasonString(String reasonString) { + properties.add(new StringProperty(MqttPropertyType.REASON_STRING, reasonString)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttUnSubAckProperties + */ + public MqttUnSubAckProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttUnSubAckProperties + */ + public MqttUnSubAckProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttUnSubscribeProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttUnSubscribeProperties.java new file mode 100644 index 0000000..bc665d7 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttUnSubscribeProperties.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.properties.UserProperty; + +/** + * mqtt5 取消订阅属性 + * + * @author L.cm + */ +public class MqttUnSubscribeProperties { + private final MqttProperties properties; + + public MqttUnSubscribeProperties() { + this(new MqttProperties()); + } + + public MqttUnSubscribeProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttUnSubscribeProperties + */ + public MqttUnSubscribeProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttUnSubscribeProperty + */ + public MqttUnSubscribeProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttWillPublishProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttWillPublishProperties.java new file mode 100644 index 0000000..facc514 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttWillPublishProperties.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.message.properties; + +import org.dromara.mica.mqtt.codec.properties.*; + +/** + * MQTT5 遗嘱消息属性类,用于存储遗嘱消息相关的属性信息 + * + * @author L.cm + */ +public class MqttWillPublishProperties { + private final MqttProperties properties; + + public MqttWillPublishProperties() { + this(new MqttProperties()); + } + + public MqttWillPublishProperties(MqttProperties properties) { + this.properties = properties; + } + + public MqttProperties getProperties() { + return properties; + } + + /** + * 设置负载格式指示器 + * + * @param indicator 负载格式指示器 (0 表示未指定, 1 表示 UTF-8 编码) + * @return MqttWillPublishProperties + */ + public MqttWillPublishProperties setPayloadFormatIndicator(int indicator) { + properties.add(new IntegerProperty(MqttPropertyType.PAYLOAD_FORMAT_INDICATOR, indicator)); + return this; + } + + /** + * 设置消息过期时间间隔 + * + * @param interval 消息过期时间间隔(秒) + * @return MqttWillPublishProperty + */ + public MqttWillPublishProperties setMessageExpiryInterval(int interval) { + properties.add(new IntegerProperty(MqttPropertyType.MESSAGE_EXPIRY_INTERVAL, interval)); + return this; + } + + /** + * 设置关联数据 + * + * @param correlationData 关联数据 + * @return MqttWillPublishProperty + */ + public MqttWillPublishProperties setCorrelationData(byte[] correlationData) { + properties.add(new BinaryProperty(MqttPropertyType.CORRELATION_DATA, correlationData)); + return this; + } + + /** + * 设置内容类型 + * + * @param contentType 内容类型 + * @return MqttWillPublishProperty + */ + public MqttWillPublishProperties setContentType(String contentType) { + properties.add(new StringProperty(MqttPropertyType.CONTENT_TYPE, contentType)); + return this; + } + + /** + * 设置响应主题 + * + * @param responseTopic 响应主题 + * @return MqttWillPublishProperty + */ + public MqttWillPublishProperties setResponseTopic(String responseTopic) { + properties.add(new StringProperty(MqttPropertyType.RESPONSE_TOPIC, responseTopic)); + return this; + } + + /** + * 设置遗嘱延迟时间间隔 + * + * @param interval 遗嘱延迟时间间隔(秒) + * @return MqttWillPublishProperty + */ + public MqttWillPublishProperties setWillDelayInterval(int interval) { + properties.add(new IntegerProperty(MqttPropertyType.WILL_DELAY_INTERVAL, interval)); + return this; + } + + /** + * 设置用户属性 + * + * @param userProperty 用户属性 + * @return MqttWillPublishProperty + */ + public MqttWillPublishProperties addUserProperty(UserProperty userProperty) { + properties.add(userProperty); + return this; + } + + /** + * 添加用户属性 + * + * @param key key + * @param value value + * @return MqttWillPublishProperty + */ + public MqttWillPublishProperties addUserProperty(String key, String value) { + this.addUserProperty(new UserProperty(key, value)); + return this; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/BinaryProperty.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/BinaryProperty.java new file mode 100644 index 0000000..b630567 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/BinaryProperty.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.properties; + +public final class BinaryProperty extends MqttProperty { + + public BinaryProperty(MqttPropertyType propertyType, byte[] value) { + super(propertyType.value(), value); + } + + public BinaryProperty(int propertyId, byte[] value) { + super(propertyId, value); + } + + @Override + public String toString() { + return "BinaryProperty(" + propertyId + ", " + value.length + " bytes)"; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/BooleanProperty.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/BooleanProperty.java new file mode 100644 index 0000000..9ed9fad --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/BooleanProperty.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.properties; + +public final class BooleanProperty extends MqttProperty { + + public BooleanProperty(MqttPropertyType propertyType, boolean value) { + this(propertyType.value(), value); + } + + public BooleanProperty(int propertyId, boolean value) { + super(propertyId, value ? 1 : 0); + } + + @Override + public String toString() { + return "BooleanProperty(" + propertyId + ", " + value + ')'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/IntegerProperty.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/IntegerProperty.java new file mode 100644 index 0000000..5c786fa --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/IntegerProperty.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.properties; + +public final class IntegerProperty extends MqttProperty { + + public IntegerProperty(MqttPropertyType propertyType, Integer value) { + super(propertyType.value(), value); + } + + public IntegerProperty(int propertyId, Integer value) { + super(propertyId, value); + } + + @Override + public String toString() { + return "IntegerProperty(" + propertyId + ", " + value + ')'; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttProperties.java new file mode 100644 index 0000000..77f49c8 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttProperties.java @@ -0,0 +1,193 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.properties; + +import java.util.*; + +/** + * MQTT Properties container + * + * @author netty + */ +public final class MqttProperties { + public static final MqttProperties NO_PROPERTIES = new MqttProperties(false); + private final boolean canModify; + private Map props; + private List userProperties; + private List subscriptionIds; + public MqttProperties() { + this(true); + } + private MqttProperties(boolean canModify) { + this.canModify = canModify; + } + + public static MqttProperties withEmptyDefaults(MqttProperties properties) { + if (properties == null) { + return MqttProperties.NO_PROPERTIES; + } + return properties; + } + + public void add(MqttProperty property) { + if (!canModify) { + throw new UnsupportedOperationException("adding property isn't allowed"); + } + Map props = this.props; + int propertyId = property.propertyId(); + if (propertyId == MqttPropertyType.USER_PROPERTY.value()) { + List userProperties = this.userProperties; + if (userProperties == null) { + userProperties = new ArrayList<>(1); + this.userProperties = userProperties; + } + if (property instanceof UserProperty) { + userProperties.add((UserProperty) property); + } else if (property instanceof UserProperties) { + for (StringPair pair : ((UserProperties) property).value()) { + userProperties.add(new UserProperty(pair.key, pair.value)); + } + } else { + throw new IllegalArgumentException("User property must be of UserProperty or UserProperties type"); + } + } else if (propertyId == MqttPropertyType.SUBSCRIPTION_IDENTIFIER.value()) { + List subscriptionIds = this.subscriptionIds; + if (subscriptionIds == null) { + subscriptionIds = new ArrayList<>(1); + this.subscriptionIds = subscriptionIds; + } + if (property instanceof IntegerProperty) { + subscriptionIds.add((IntegerProperty) property); + } else { + throw new IllegalArgumentException("Subscription ID must be an integer property"); + } + } else { + if (props == null) { + props = new HashMap<>(); + this.props = props; + } + props.put(propertyId, property); + } + } + + public Collection listAll() { + Map props = this.props; + if (props == null && subscriptionIds == null && userProperties == null) { + return Collections.emptyList(); + } + if (props == null && userProperties == null) { + return subscriptionIds; + } + if (props == null && subscriptionIds == null) { + return userProperties; + } + if (subscriptionIds == null && userProperties == null) { + return props.values(); + } + List propValues = new ArrayList<>(props != null ? props.size() : 1); + if (props != null) { + propValues.addAll(props.values()); + } + if (subscriptionIds != null) { + propValues.addAll(subscriptionIds); + } + if (userProperties != null) { + propValues.add(UserProperties.fromUserPropertyCollection(userProperties)); + } + return propValues; + } + + public boolean isEmpty() { + Map props = this.props; + return props == null || props.isEmpty(); + } + + /** + * Get property by ID. If there are multiple properties of this type (can be with Subscription ID) + * then return the first one. + * + * @param propertyId ID of the property + * @return a property if it is set, null otherwise + */ + public MqttProperty getProperty(int propertyId) { + if (MqttPropertyType.USER_PROPERTY.value() == propertyId) { + //special handling to keep compatibility with earlier versions + List userProperties = this.userProperties; + if (userProperties == null) { + return null; + } + return UserProperties.fromUserPropertyCollection(userProperties); + } + if (MqttPropertyType.SUBSCRIPTION_IDENTIFIER.value() == propertyId) { + List subscriptionIds = this.subscriptionIds; + if (subscriptionIds == null || subscriptionIds.isEmpty()) { + return null; + } + return subscriptionIds.get(0); + } + Map props = this.props; + return props == null ? null : props.get(propertyId); + } + + /** + * Get property by ID. If there are multiple properties of this type (can be with Subscription ID) + * then return the first one. + * + * @param mqttPropertyType Type of the property + * @return a property if it is set, null otherwise + */ + public MqttProperty getProperty(MqttPropertyType mqttPropertyType) { + return getProperty(mqttPropertyType.value()); + } + + /** + * Get property by ID. If there are multiple properties of this type (can be with Subscription ID) + * then return the first one. + * + * @param mqttPropertyType Type of the property + * @param 泛型标记 + * @return a property value if it is set, null otherwise + */ + public T getPropertyValue(MqttPropertyType mqttPropertyType) { + MqttProperty property = getProperty(mqttPropertyType.value()); + if (property == null) { + return null; + } + return (T) property.value(); + } + + /** + * Get properties by ID. + * Some properties (Subscription ID and User Properties) may occur multiple times, + * this method returns all their values in order. + * + * @param propertyId ID of the property + * @return all properties having specified ID + */ + public List getProperties(int propertyId) { + if (propertyId == MqttPropertyType.USER_PROPERTY.value()) { + return userProperties == null ? Collections.emptyList() : userProperties; + } + if (propertyId == MqttPropertyType.SUBSCRIPTION_IDENTIFIER.value()) { + return subscriptionIds == null ? Collections.emptyList() : subscriptionIds; + } + Map props = this.props; + return (props == null || !props.containsKey(propertyId)) ? + Collections.emptyList() : + Collections.singletonList(props.get(propertyId)); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttProperty.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttProperty.java new file mode 100644 index 0000000..cbeffe4 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttProperty.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.properties; + +/** + * MQTT property base class + * + * @param property type + */ +public abstract class MqttProperty { + final int propertyId; + final T value; + + protected MqttProperty(int propertyId, T value) { + this.propertyId = propertyId; + this.value = value; + } + + /** + * Get MQTT property ID + * + * @return property ID + */ + public int propertyId() { + return propertyId; + } + + /** + * Get MQTT property value + * + * @return property value + */ + public T value() { + return value; + } + + @Override + public int hashCode() { + return propertyId + 31 * value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + MqttProperty that = (MqttProperty) obj; + return this.propertyId == that.propertyId && this.value.equals(that.value); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttPropertyType.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttPropertyType.java new file mode 100644 index 0000000..0d717c5 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttPropertyType.java @@ -0,0 +1,175 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.properties; + +/** + * mqtt 属性类型 + * + * @author netty、L.cm + */ +public enum MqttPropertyType { + // single byte properties + /** + * 有效载荷标识(Payload Format Indicator),该属性只存在于 PUBLISH 报文和 CONNECT 报文的遗嘱属性中。 + */ + PAYLOAD_FORMAT_INDICATOR((byte) 0x01), + /** + * 请求问题信息 + */ + REQUEST_PROBLEM_INFORMATION((byte) 0x17), + /** + * 请求响应信息 + */ + REQUEST_RESPONSE_INFORMATION((byte) 0x19), + /** + * 服务器支持得最高 qos 级别 + */ + MAXIMUM_QOS((byte) 0x24), + /** + * 保留消息可用 + */ + RETAIN_AVAILABLE((byte) 0x25), + /** + * 订阅通配符可用 + */ + WILDCARD_SUBSCRIPTION_AVAILABLE((byte) 0x28), + /** + * 订阅标识符可用 + */ + SUBSCRIPTION_IDENTIFIER_AVAILABLE((byte) 0x29), + /** + * $share 共享订阅可用 + */ + SHARED_SUBSCRIPTION_AVAILABLE((byte) 0x2A), + + // two bytes properties + /** + * 服务器 keep alive + */ + SERVER_KEEP_ALIVE((byte) 0x13), + /** + * 告知对方自己希望处理未决的最大的 Qos1 或者 Qos2 PUBLISH消息个数,如果不存在,则默认是65535。作用:流控。 + */ + RECEIVE_MAXIMUM((byte) 0x21), + /** + * topic 别名最大值 + */ + TOPIC_ALIAS_MAXIMUM((byte) 0x22), + /** + * topic 别名 + */ + TOPIC_ALIAS((byte) 0x23), + + // four bytes properties + MESSAGE_EXPIRY_INTERVAL((byte) 0x02), + /** + * session 超时时间,连接时使用 + */ + SESSION_EXPIRY_INTERVAL((byte) 0x11), + /** + * 遗嘱消息延迟时间 + */ + WILL_DELAY_INTERVAL((byte) 0x18), + /** + * 最大包体大小 + */ + MAXIMUM_PACKET_SIZE((byte) 0x27), + + // Variable Byte Integer + /** + * 订阅标识符 + */ + SUBSCRIPTION_IDENTIFIER((byte) 0x0B), + + // UTF-8 Encoded String properties + /** + * 内容类型(Content Type),只存在于 PUBLISH 报文和 CONNECT 报文的遗嘱属性中。 + * 例如:存放 MIME 类型,比如 text/plain 表示文本文件,audio/aac 表示音频文件。 + */ + CONTENT_TYPE((byte) 0x03), + /** + * 响应的 topic + */ + RESPONSE_TOPIC((byte) 0x08), + /** + * 指定的客户标识符 + */ + ASSIGNED_CLIENT_IDENTIFIER((byte) 0x12), + /** + * 身份验证方法 + */ + AUTHENTICATION_METHOD((byte) 0x15), + /** + * 响应信息 + */ + RESPONSE_INFORMATION((byte) 0x1A), + /** + * 服务器参考 + */ + SERVER_REFERENCE((byte) 0x1C), + /** + * 所有的ACK以及DISCONNECT 都可以携带 Reason String属性告知对方一些特殊的信息, + * 一般来说是ACK失败的情况下会使用该属性告知对端为什么失败,可用来弥补Reason Code信息不够。 + */ + REASON_STRING((byte) 0x1F), + /** + * 用户属性 + */ + USER_PROPERTY((byte) 0x26), + + // Binary Data + /** + * 相关数据 + */ + CORRELATION_DATA((byte) 0x09), + /** + * 认证数据 + */ + AUTHENTICATION_DATA((byte) 0x16); + + private static final MqttPropertyType[] VALUES; + + static { + VALUES = new MqttPropertyType[43]; + for (MqttPropertyType v : values()) { + VALUES[v.value] = v; + } + } + + private final byte value; + + MqttPropertyType(byte value) { + this.value = value; + } + + public static MqttPropertyType valueOf(int type) { + MqttPropertyType t = null; + try { + t = VALUES[type]; + } catch (ArrayIndexOutOfBoundsException ignored) { + // nop + } + if (t == null) { + throw new IllegalArgumentException("unknown property type: " + type); + } + return t; + } + + public byte value() { + return value; + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/StringPair.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/StringPair.java new file mode 100644 index 0000000..3ea79e5 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/StringPair.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.properties; + +public final class StringPair { + public final String key; + public final String value; + + public StringPair(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public int hashCode() { + return key.hashCode() + 31 * value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + StringPair that = (StringPair) obj; + + return that.key.equals(this.key) && that.value.equals(this.value); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/StringProperty.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/StringProperty.java new file mode 100644 index 0000000..4e41524 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/StringProperty.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.properties; + +public final class StringProperty extends MqttProperty { + + public StringProperty(MqttPropertyType propertyType, String value) { + super(propertyType.value(), value); + } + + public StringProperty(int propertyId, String value) { + super(propertyId, value); + } + + @Override + public String toString() { + return "StringProperty(" + propertyId + ", " + value + ')'; + } + +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/UserProperties.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/UserProperties.java new file mode 100644 index 0000000..7f75b55 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/UserProperties.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.properties; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +//User properties are the only properties that may be included multiple times and +//are the only properties where ordering is required. Therefore, they need a special handling +public final class UserProperties extends MqttProperty> { + public UserProperties() { + super(MqttPropertyType.USER_PROPERTY.value(), new ArrayList<>()); + } + + /** + * Create user properties from the collection of the String pair values + * + * @param values string pairs. Collection entries are copied, collection itself isn't shared + */ + public UserProperties(Collection values) { + this(); + this.value.addAll(values); + } + + public static UserProperties fromUserPropertyCollection(Collection properties) { + UserProperties userProperties = new UserProperties(); + for (UserProperty property : properties) { + userProperties.add(new StringPair(property.value.key, property.value.value)); + } + return userProperties; + } + + public void add(StringPair pair) { + this.value.add(pair); + } + + public void add(String key, String value) { + this.value.add(new StringPair(key, value)); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("UserProperties("); + boolean first = true; + for (StringPair pair : value) { + if (!first) { + builder.append(", "); + } + builder.append(pair.key).append("->").append(pair.value); + first = false; + } + builder.append(')'); + return builder.toString(); + } +} diff --git a/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/UserProperty.java b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/UserProperty.java new file mode 100644 index 0000000..c4dc0f4 --- /dev/null +++ b/mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/UserProperty.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.dromara.mica.mqtt.codec.properties; + +public final class UserProperty extends MqttProperty { + public UserProperty(String key, String value) { + super(MqttPropertyType.USER_PROPERTY.value(), new StringPair(key, value)); + } + + @Override + public String toString() { + return "UserProperty(" + value.key + ", " + value.value + ')'; + } +} + diff --git a/mica-mqtt-codec/src/main/moditect/module-info.java b/mica-mqtt-codec/src/main/moditect/module-info.java new file mode 100644 index 0000000..f406b0d --- /dev/null +++ b/mica-mqtt-codec/src/main/moditect/module-info.java @@ -0,0 +1,12 @@ +open module org.dromara.mica.mqtt.codec { + requires net.dreamlu.mica.net.core; + exports org.dromara.mica.mqtt.codec; + exports org.dromara.mica.mqtt.codec.codes; + exports org.dromara.mica.mqtt.codec.exception; + exports org.dromara.mica.mqtt.codec.message; + exports org.dromara.mica.mqtt.codec.message.builder; + exports org.dromara.mica.mqtt.codec.message.header; + exports org.dromara.mica.mqtt.codec.message.payload; + exports org.dromara.mica.mqtt.codec.message.properties; + exports org.dromara.mica.mqtt.codec.properties; +} diff --git a/mica-mqtt-codec/src/test/java/org/dromara/mica/mqtt/codec/test/MqttCodecUtilTest.java b/mica-mqtt-codec/src/test/java/org/dromara/mica/mqtt/codec/test/MqttCodecUtilTest.java new file mode 100644 index 0000000..9a7234a --- /dev/null +++ b/mica-mqtt-codec/src/test/java/org/dromara/mica/mqtt/codec/test/MqttCodecUtilTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.codec.test; + +import org.dromara.mica.mqtt.codec.MqttCodecUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class MqttCodecUtilTest { + + @Test + public void testIsTopicFilter() { + boolean topicFilter1 = MqttCodecUtil.isTopicFilter("/test/123"); + Assertions.assertFalse(topicFilter1); + boolean topicFilter2 = MqttCodecUtil.isTopicFilter("/test/123/"); + Assertions.assertFalse(topicFilter2); + boolean topicFilter3 = MqttCodecUtil.isTopicFilter("/test/+/123"); + Assertions.assertTrue(topicFilter3); + boolean topicFilter4 = MqttCodecUtil.isTopicFilter("/test/#"); + Assertions.assertTrue(topicFilter4); + } + +} diff --git a/mica-mqtt-common/README.md b/mica-mqtt-common/README.md new file mode 100644 index 0000000..2844ba8 --- /dev/null +++ b/mica-mqtt-common/README.md @@ -0,0 +1,2 @@ +# 公用的工具类 + diff --git a/mica-mqtt-common/pom.xml b/mica-mqtt-common/pom.xml new file mode 100644 index 0000000..a09b0ad --- /dev/null +++ b/mica-mqtt-common/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + mica-mqtt + ${revision} + + mica-mqtt-common + ${project.artifactId} + https://mica-mqtt.dreamlu.net + + + + org.dromara.mica-mqtt + mica-mqtt-codec + + + net.dreamlu + mica-net-core + + + + org.tinylog + slf4j-tinylog + test + + + org.tinylog + tinylog-impl + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + org.moditect + moditect-maven-plugin + + + + + diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttClientPublish.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttClientPublish.java new file mode 100644 index 0000000..b50f089 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttClientPublish.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.annotation; + +import org.dromara.mica.mqtt.codec.MqttQoS; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 客户端发布注解 + * + * @author ChangJin Wei (魏昌进) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface MqttClientPublish { + + /** + * 订阅的 topic + * + * @return topic + */ + String value(); + + /** + * 发布的 qos + * + * @return MqttQoS + */ + MqttQoS qos() default MqttQoS.QOS0; + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttClientSubscribe.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttClientSubscribe.java new file mode 100644 index 0000000..7a8a60b --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttClientSubscribe.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.annotation; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.deserialize.MqttJsonDeserializer; + +import java.lang.annotation.*; + +/** + * 客户端订阅注解 + * + * @author L.cm + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface MqttClientSubscribe { + + /** + * 默认的客户端模板 bean 名称 + */ + String DEFAULT_CLIENT_TEMPLATE_BEAN = "mqttClientTemplate"; + + /** + * 订阅的 topic filter + * + * @return topic filter + */ + String[] value(); + + /** + * 订阅的 qos + * + * @return MqttQoS + */ + MqttQoS qos() default MqttQoS.QOS0; + + /** + * mqtt 消息反序列化 + * + * @return 反序列化 + */ + Class deserialize() default MqttJsonDeserializer.class; + + /** + * 客户端 bean 名称 + * + * @return bean name + */ + String clientTemplateBean() default MqttClientSubscribe.DEFAULT_CLIENT_TEMPLATE_BEAN; + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttPayload.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttPayload.java new file mode 100644 index 0000000..380dbd2 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttPayload.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author ChangJin Wei (魏昌进) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface MqttPayload { + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttRetain.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttRetain.java new file mode 100644 index 0000000..08f2cfd --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttRetain.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 是否在服务器上保留消息 + * @author ChangJin Wei (魏昌进) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface MqttRetain { + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttServerFunction.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttServerFunction.java new file mode 100644 index 0000000..dadb4db --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttServerFunction.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.annotation; + +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.deserialize.MqttJsonDeserializer; + +import java.lang.annotation.*; + +/** + * 服务端函数 + * + * @author L.cm + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface MqttServerFunction { + + /** + * 订阅的 topic filter + * + * @return topic filter + */ + String[] value(); + + /** + * mqtt 消息反序列化 + * + * @return 反序列化 + */ + Class deserialize() default MqttJsonDeserializer.class; + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/MqttPendingPublish.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/MqttPendingPublish.java new file mode 100644 index 0000000..cfdfc40 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/MqttPendingPublish.java @@ -0,0 +1,93 @@ +package org.dromara.mica.mqtt.core.common; + +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; +import org.tio.utils.timer.TimerTaskService; + +import java.util.Objects; + +/** + * MqttPendingPublish,参考于 netty-mqtt-client + * + * @author netty + */ +public final class MqttPendingPublish { + private static final Logger logger = LoggerFactory.getLogger(MqttPendingPublish.class); + private final MqttPublishMessage message; + private final MqttQoS qos; + private final RetryProcessor pubRetryProcessor = new RetryProcessor<>(); + private final RetryProcessor pubRelRetryProcessor = new RetryProcessor<>(); + + public MqttPendingPublish(MqttPublishMessage message, MqttQoS qos) { + this.message = message; + this.qos = qos; + this.pubRetryProcessor.setOriginalMessage(message); + } + + public MqttPublishMessage getMessage() { + return message; + } + + public MqttQoS getQos() { + return qos; + } + + public void startPublishRetransmissionTimer(TimerTaskService taskService, ChannelContext context) { + this.pubRetryProcessor.setHandle(((fixedHeader, originalMessage) -> { + MqttPublishMessage publishMessage = new MqttPublishMessage(fixedHeader, originalMessage.variableHeader(), this.message.payload()); + boolean result = Tio.send(context, publishMessage); + if (context.isServer()) { + logger.info("retry send Publish msg clientId:{} qos:{} result:{}", context.getBsId(), qos, result); + } else { + logger.info("retry send Publish msg qos:{} result:{}", qos, result); + } + })); + this.pubRetryProcessor.start(taskService); + } + + public void onPubAckReceived() { + this.pubRetryProcessor.stop(); + } + + public void setPubRelMessage(MqttMessage pubRelMessage) { + this.pubRelRetryProcessor.setOriginalMessage(pubRelMessage); + } + + public void startPubRelRetransmissionTimer(TimerTaskService taskService, ChannelContext context) { + this.pubRelRetryProcessor.setHandle((fixedHeader, originalMessage) -> { + boolean result = Tio.send(context, new MqttMessage(fixedHeader, originalMessage.variableHeader())); + if (context.isServer()) { + logger.info("retry send PubRel msg clientId:{} qos:{} result:{}", context.getBsId(), qos, result); + } else { + logger.info("retry send PubRel msg qos:{} result:{}", qos, result); + } + }); + this.pubRelRetryProcessor.start(taskService); + } + + public void onPubCompReceived() { + this.pubRelRetryProcessor.stop(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MqttPendingPublish that = (MqttPendingPublish) o; + return Objects.equals(message, that.message) && qos == that.qos; + } + + @Override + public int hashCode() { + return Objects.hash(message, qos); + } +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/MqttPendingQos2Publish.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/MqttPendingQos2Publish.java new file mode 100644 index 0000000..288d2d8 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/MqttPendingQos2Publish.java @@ -0,0 +1,63 @@ +package org.dromara.mica.mqtt.core.common; + +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; +import org.tio.utils.timer.TimerTaskService; + +import java.util.Objects; + +/** + * MqttPendingPublish,参考于 netty-mqtt-client + */ +public final class MqttPendingQos2Publish { + private static final Logger logger = LoggerFactory.getLogger(MqttPendingQos2Publish.class); + private final MqttPublishMessage incomingPublish; + private final RetryProcessor retryProcessor = new RetryProcessor<>(); + + public MqttPendingQos2Publish(MqttPublishMessage incomingPublish, MqttMessage originalMessage) { + this.incomingPublish = incomingPublish; + this.retryProcessor.setOriginalMessage(originalMessage); + } + + public MqttPublishMessage getIncomingPublish() { + return incomingPublish; + } + + public void startPubRecRetransmitTimer(TimerTaskService taskService, ChannelContext context) { + this.retryProcessor.setHandle((fixedHeader, originalMessage) -> { + boolean result = Tio.send(context, new MqttMessage(fixedHeader, originalMessage.variableHeader())); + if (context.isServer()) { + logger.info("retry send PubRec msg clientId:{} result:{}", context.getBsId(), result); + } else { + logger.info("retry send PubRec msg result:{}", result); + } + }); + this.retryProcessor.start(taskService); + } + + public void onPubRelReceived() { + this.retryProcessor.stop(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MqttPendingQos2Publish that = (MqttPendingQos2Publish) o; + return Objects.equals(incomingPublish, that.incomingPublish); + } + + @Override + public int hashCode() { + return Objects.hash(incomingPublish); + } + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/RetryProcessor.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/RetryProcessor.java new file mode 100644 index 0000000..a6b59d8 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/RetryProcessor.java @@ -0,0 +1,51 @@ +package org.dromara.mica.mqtt.core.common; + + +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.core.util.timer.AckTimerTask; +import org.tio.utils.timer.TimerTaskService; + +import java.util.Objects; +import java.util.function.BiConsumer; + +/** + * 重试处理器,参考于 netty-mqtt-client + * + * @param MqttMessage + */ +public final class RetryProcessor { + + private AckTimerTask ackTimerTask; + private BiConsumer handler; + private T originalMessage; + + public void start(TimerTaskService taskService) { + Objects.requireNonNull(this.handler, "RetryProcessor handler is null."); + this.startTimer(Objects.requireNonNull(taskService, "RetryProcessor taskService is null.")); + } + + private void startTimer(TimerTaskService taskService) { + this.ackTimerTask = taskService.addTask((systemTimer) -> { + return new AckTimerTask(systemTimer, () -> { + MqttFixedHeader fixedHeader = new MqttFixedHeader(this.originalMessage.fixedHeader().messageType(), true, this.originalMessage.fixedHeader().qosLevel(), this.originalMessage.fixedHeader().isRetain(), this.originalMessage.fixedHeader().remainingLength()); + handler.accept(fixedHeader, originalMessage); + }, 5, 10); + }); + } + + public void stop() { + if (this.ackTimerTask != null) { + this.ackTimerTask.cancel(); + } + } + + public void setHandle(BiConsumer runnable) { + this.handler = runnable; + } + + public void setOriginalMessage(T originalMessage) { + this.originalMessage = originalMessage; + } + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicFilter.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicFilter.java new file mode 100644 index 0000000..2850260 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicFilter.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.common; + +/** + * TopicFilter + * + * @author L.cm + */ +public class TopicFilter { + + /** + * topicFilter + */ + private final String topic; + /** + * topicFilterType + */ + private final TopicFilterType type; + + public TopicFilter(String topicFilter) { + this.topic = topicFilter; + this.type = TopicFilterType.getType(topicFilter); + } + + public String getTopic() { + return topic; + } + + public TopicFilterType getType() { + return type; + } + + /** + * 判断 topicFilter 和 topicName 匹配情况 + * + * @param topicName topicName + * @return 是否匹配 + */ + public boolean match(String topicName) { + return type.match(this.topic, topicName); + } + + @Override + public String toString() { + return "TopicFilter{" + + "topic='" + topic + '\'' + + ", type=" + type + + '}'; + } +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicFilterType.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicFilterType.java new file mode 100644 index 0000000..967c5da --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicFilterType.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.common; + +import org.dromara.mica.mqtt.core.util.TopicUtil; + +import java.util.Map; + +/** + * TopicFilter 类型 + * + * @author L.cm + */ +public enum TopicFilterType { + + /** + * 默认 TopicFilter + */ + NONE { + @Override + public boolean match(String topicFilter, String topicName) { + return TopicUtil.match(topicFilter, topicName); + } + + @Override + public Map getTopicVars(String topicTemplate, String topicName) { + return TopicUtil.getTopicVars(topicTemplate, topicName); + } + }, + + /** + * $queue/ 为前缀的共享订阅是不带群组的共享订阅 + */ + QUEUE { + @Override + public boolean match(String topicFilter, String topicName) { + // $queue/ 共享订阅前缀去除 + int prefixLen = TopicFilterType.SHARE_QUEUE_PREFIX.length(); + return TopicUtil.match(topicFilter.substring(prefixLen), topicName); + } + + @Override + public Map getTopicVars(String topicTemplate, String topicName) { + // $queue/ 共享订阅前缀去除 + int prefixLen = TopicFilterType.SHARE_QUEUE_PREFIX.length(); + return TopicUtil.getTopicVars(topicTemplate.substring(prefixLen), topicName); + } + }, + + /** + * $share/{group-name}/ 为前缀的共享订阅是带群组的共享订阅 + */ + SHARE { + @Override + public boolean match(String topicFilter, String topicName) { + // 去除前缀 $share// ,匹配 topicName / 前缀 + int prefixLen = TopicFilterType.findShareTopicIndex(topicFilter); + return TopicUtil.match(topicFilter.substring(prefixLen), topicName); + } + + @Override + public Map getTopicVars(String topicTemplate, String topicName) { + // 去除前缀 $share// ,匹配 topicName / 前缀 + int prefixLen = TopicFilterType.findShareTopicIndex(topicTemplate); + return TopicUtil.getTopicVars(topicTemplate.substring(prefixLen), topicName); + } + }; + + /** + * 共享订阅的 topic + */ + public static final String SHARE_QUEUE_PREFIX = "$queue/"; + public static final String SHARE_GROUP_PREFIX = "$share/"; + + /** + * 判断 topicFilter 和 topicName 匹配情况 + * + * @param topicFilter topicFilter + * @param topicName topicName + * @return 是否匹配 + */ + public abstract boolean match(String topicFilter, String topicName); + + /** + * 解析 topic 模板中的变量 例如 $SYS/brokers/${node}/clients/${clientid}/disconnected 中提取 node 和 clientid + * + * @param topicTemplate topicTemplate + * @param topic topic + * @return 提取的变量 + */ + public abstract Map getTopicVars(String topicTemplate, String topic); + + /** + * 获取 topicFilter 类型 + * + * @param topicFilter topicFilter + * @return TopicFilterType + */ + public static TopicFilterType getType(String topicFilter) { + if (topicFilter.startsWith(TopicFilterType.SHARE_QUEUE_PREFIX)) { + return TopicFilterType.QUEUE; + } else if (topicFilter.startsWith(TopicFilterType.SHARE_GROUP_PREFIX)) { + return TopicFilterType.SHARE; + } else { + return TopicFilterType.NONE; + } + } + + /** + * 读取共享订阅的分组名 + * + * @param topicFilter topicFilter + * @return 共享订阅分组名 + */ + public static String getShareGroupName(String topicFilter) { + int prefixLength = TopicFilterType.SHARE_GROUP_PREFIX.length(); + int topicFilterLength = topicFilter.length(); + for (int i = prefixLength; i < topicFilterLength; i++) { + char ch = topicFilter.charAt(i); + if ('/' == ch) { + return topicFilter.substring(prefixLength, i); + } + } + throw new IllegalArgumentException("共享订阅 topicFilter: " + topicFilter + " 不符合规范 $share//xxx"); + } + + private static int findShareTopicIndex(String topicFilter) { + int prefixLength = TopicFilterType.SHARE_GROUP_PREFIX.length(); + int topicFilterLength = topicFilter.length(); + for (int i = prefixLength; i < topicFilterLength; i++) { + char ch = topicFilter.charAt(i); + if ('/' == ch) { + return i + 1; + } + } + throw new IllegalArgumentException("共享订阅 topicFilter: " + topicFilter + " 不符合规范 $share//xxx"); + } + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicTemplate.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicTemplate.java new file mode 100644 index 0000000..fd00b1e --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicTemplate.java @@ -0,0 +1,43 @@ +package org.dromara.mica.mqtt.core.common; + +import org.tio.utils.mica.StrTemplateParser; + +import java.util.Map; + +/** + * topic 模板带 ${var} 变量的模板 + * + * @author L.cm + */ +public class TopicTemplate { + private final StrTemplateParser templateParser; + private final String topicFilter; + private final TopicFilterType type; + + public TopicTemplate(String topicTemplate, String topicFilter) { + this.templateParser = new StrTemplateParser(topicTemplate); + this.topicFilter = topicFilter; + this.type = TopicFilterType.getType(topicFilter); + } + + /** + * 判断 topicFilter 和 topicName 匹配情况 + * + * @param topicName topicName + * @return 是否匹配 + */ + public boolean match(String topicName) { + return type.match(this.topicFilter, topicName); + } + + /** + * 解析 topic 中的变量 + * + * @param topicName topicName + * @return 变量 + */ + public Map getVariables(String topicName) { + return templateParser.getVariables(topicName); + } + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/deserialize/MqttDeserializer.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/deserialize/MqttDeserializer.java new file mode 100644 index 0000000..1fbf81d --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/deserialize/MqttDeserializer.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.deserialize; + +import java.lang.reflect.Type; + +/** + * mqtt 消息反序列化 + * + * @author L.cm + * @author ChangJin Wei(魏昌进) + */ +public interface MqttDeserializer { + + /** + * 反序列化 + * + * @param bytes bytes + * @param type type + * @param 泛型 + * @return T + */ + T deserialize(byte[] bytes, Type type); + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/deserialize/MqttJsonDeserializer.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/deserialize/MqttJsonDeserializer.java new file mode 100644 index 0000000..1f238f7 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/deserialize/MqttJsonDeserializer.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.deserialize; + + +import org.tio.utils.json.JsonUtil; + +import java.lang.reflect.Type; + +/** + * mqtt 消息反序列化 + * + * @author L.cm + * @author ChangJin Wei(魏昌进) + */ +public class MqttJsonDeserializer implements MqttDeserializer { + + @Override + public T deserialize(byte[] bytes, Type type) { + return JsonUtil.readValue(bytes, type); + } +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ObjectParamValueFunction.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ObjectParamValueFunction.java new file mode 100644 index 0000000..8d7fe7b --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ObjectParamValueFunction.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.function; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.tio.core.ChannelContext; + +import java.lang.reflect.Type; + +/** + * 需要序列化的对象函数 + * + * @author L.cm + */ +public class ObjectParamValueFunction implements ParamValueFunction { + private final MqttDeserializer deserializer; + private final Type parameterType; + + public ObjectParamValueFunction(MqttDeserializer deserializer, Type parameterType) { + this.deserializer = deserializer; + this.parameterType = parameterType; + } + + @Override + public Object getValue(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + return deserializer.deserialize(payload, parameterType); + } +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ParamValueFunction.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ParamValueFunction.java new file mode 100644 index 0000000..7e74459 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ParamValueFunction.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.function; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.tio.core.ChannelContext; + +/** + * 参数值函数 + * + * @author L.cm + */ +@FunctionalInterface +public interface ParamValueFunction { + + /** + * 获取值 + * + * @param context ChannelContext + * @param topic topic + * @param message message + * @param payload payload + * @return value + */ + Object getValue(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload); + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ParamValueFunctions.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ParamValueFunctions.java new file mode 100644 index 0000000..42bc12c --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ParamValueFunctions.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.function; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.tio.core.ChannelContext; + +import java.nio.ByteBuffer; + +/** + * 参数值函数 + * + * @author L.cm + */ +public enum ParamValueFunctions implements ParamValueFunction { + + Context() { + @Override + public Object getValue(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + return context; + } + }, + + Topic() { + @Override + public Object getValue(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + return topic; + } + }, + + Message() { + @Override + public Object getValue(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + return message; + } + }, + + Payload() { + @Override + public Object getValue(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + return payload; + } + }, + + ByteBuff() { + @Override + public Object getValue(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + return ByteBuffer.wrap(payload); + } + }; + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/TopicVarsParamValueFunction.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/TopicVarsParamValueFunction.java new file mode 100644 index 0000000..8aa7df7 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/TopicVarsParamValueFunction.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.function; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.common.TopicTemplate; +import org.tio.core.ChannelContext; + +import java.util.Collections; + +/** + * topic 参数函数 + * + * @author L.cm + */ +public class TopicVarsParamValueFunction implements ParamValueFunction { + private final TopicTemplate[] topicTemplates; + + public TopicVarsParamValueFunction(String[] topicTemplates, String[] topicFilters) { + this.topicTemplates = getTopicTemplates(topicTemplates, topicFilters); + } + + /** + * 获取 topic 模板列表 + * + * @param topicTemplates topicTemplates + * @param topicFilters topicFilters + * @return TopicTemplate array + */ + private static TopicTemplate[] getTopicTemplates(String[] topicTemplates, String[] topicFilters) { + TopicTemplate[] templates = new TopicTemplate[topicTemplates.length]; + for (int i = 0; i < templates.length; i++) { + templates[i] = new TopicTemplate(topicTemplates[i], topicFilters[i]); + } + return templates; + } + + @Override + public Object getValue(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + // 大部分情况下只有一个 topic,直接返回 + int length = topicTemplates.length; + if (length == 1) { + return topicTemplates[0].getVariables(topic); + } + // 多个 topic 时,需要根据 topic 匹配 + for (TopicTemplate topicTemplate : topicTemplates) { + if (topicTemplate.match(topic)) { + return topicTemplate.getVariables(topic); + } + } + return Collections.emptyMap(); + } + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/serializer/MqttJsonSerializer.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/serializer/MqttJsonSerializer.java new file mode 100644 index 0000000..b846ccb --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/serializer/MqttJsonSerializer.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.serializer; + +import org.tio.utils.json.JsonUtil; + +/** + * mqtt 消息 json 序列化 + * + * @author L.cm + */ +public class MqttJsonSerializer implements MqttSerializer { + + @Override + public byte[] serialize(Object message) { + return JsonUtil.toJsonBytes(message); + } + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/serializer/MqttSerializer.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/serializer/MqttSerializer.java new file mode 100644 index 0000000..388d815 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/serializer/MqttSerializer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.serializer; + +/** + * mqtt 消息序列化 + * + * @author L.cm + */ +public interface MqttSerializer { + + /** + * 序列化 + * + * @param message message + * @return byte 数组 + */ + byte[] serialize(Object message); + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/MethodParamUtil.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/MethodParamUtil.java new file mode 100644 index 0000000..9daff38 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/MethodParamUtil.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.util; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.function.ObjectParamValueFunction; +import org.dromara.mica.mqtt.core.function.ParamValueFunction; +import org.dromara.mica.mqtt.core.function.ParamValueFunctions; +import org.dromara.mica.mqtt.core.function.TopicVarsParamValueFunction; +import org.tio.core.ChannelContext; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.util.Map; + +/** + * 方法参数工具 + * + * @author L.cm + */ +public class MethodParamUtil { + + /** + * 获取参数值函数 + * + * @param method 方法 + * @param topicTemplates 主题模板 + * @param topicFilters 主题过滤器 + * @param deserializer 反序列化 + * @return ParamValueFunc[] + */ + public static ParamValueFunction[] getParamValueFunctions(Method method, String[] topicTemplates, String[] topicFilters, MqttDeserializer deserializer) { + int parameterCount = method.getParameterCount(); + ParamValueFunction[] functions = new ParamValueFunction[parameterCount]; + Type[] parameterTypes = method.getGenericParameterTypes(); + for (int i = 0; i < parameterCount; i++) { + Type parameterType = parameterTypes[i]; + if (parameterType == ChannelContext.class) { + functions[i] = ParamValueFunctions.Context; + } else if (parameterType == String.class) { + functions[i] = ParamValueFunctions.Topic; + } else if (parameterType instanceof ParameterizedType && isStringStringMap(parameterType)) { + functions[i] = new TopicVarsParamValueFunction(topicTemplates, topicFilters); + } else if (parameterType == MqttPublishMessage.class) { + functions[i] = ParamValueFunctions.Message; + } else if (parameterType == byte[].class) { + functions[i] = ParamValueFunctions.Payload; + } else if (parameterType == ByteBuffer.class) { + functions[i] = ParamValueFunctions.ByteBuff; + } else { + functions[i] = new ObjectParamValueFunction(deserializer, parameterType); + } + } + return functions; + } + + /** + * 判断是否 Map String String + * + * @param parameterType parameterType + * @return 是否 Map String String + */ + public static boolean isStringStringMap(Type parameterType) { + ParameterizedType parameterizedType = (ParameterizedType) parameterType; + Type rawType = parameterizedType.getRawType(); + // 检查是否为 Map 类型 + if (rawType != Map.class) { + return false; + } + // 获取泛型参数 + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + // 检查键和值类型是否为 String + return typeArguments[0].equals(String.class) && typeArguments[1].equals(String.class); + } + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/TopicUtil.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/TopicUtil.java new file mode 100644 index 0000000..4964764 --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/TopicUtil.java @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.util; + +import org.dromara.mica.mqtt.codec.MqttCodecUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.utils.hutool.StrUtil; +import org.tio.utils.mica.Pair; +import org.tio.utils.mica.StrTemplateParser; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +/** + * Mqtt Topic 工具 + * + * @author L.cm + */ +public final class TopicUtil { + private static final Logger logger = LoggerFactory.getLogger(TopicUtil.class); + public static final String TOPIC_LAYER = "/"; + public static final String TOPIC_WILDCARDS_ONE = "+"; + public static final String TOPIC_WILDCARDS_MORE = "#"; + + /** + * 校验 topicFilter + * + * @param topicFilterList topicFilter 集合 + */ + public static void validateTopicFilter(List topicFilterList) { + for (String topicFilter : topicFilterList) { + validateTopicFilter(topicFilter); + } + } + + /** + * 校验 topicFilter + * + * @param topicFilter topicFilter + */ + public static void validateTopicFilter(String topicFilter) throws IllegalArgumentException { + if (StrUtil.isEmpty(topicFilter)) { + throw new IllegalArgumentException("TopicFilter is empty:" + topicFilter); + } + char[] topicFilterChars = topicFilter.toCharArray(); + int topicFilterLength = topicFilterChars.length; + int topicFilterIdxEnd = topicFilterLength - 1; + char ch; + for (int i = 0; i < topicFilterLength; i++) { + ch = topicFilterChars[i]; + if (Character.isWhitespace(ch)) { + logger.warn("注意:topic:[{}] 中包含空白字符串:[{}],请检查是否正确", topicFilter, ch); + } else if (ch == MqttCodecUtil.TOPIC_WILDCARDS_MORE) { + // 校验: # 通配符只能在最后一位 + if (i < topicFilterIdxEnd) { + throw new IllegalArgumentException("Mqtt subscribe topicFilter illegal:" + topicFilter); + } + } else if (ch == MqttCodecUtil.TOPIC_WILDCARDS_ONE) { + // 校验: 单独 + 是允许的,判断 + 号前一位是否为 /,如果有后一位也必须为 / + if ((i > 0 && topicFilterChars[i - 1] != MqttCodecUtil.TOPIC_LAYER) || (i < topicFilterIdxEnd && topicFilterChars[i + 1] != MqttCodecUtil.TOPIC_LAYER)) { + throw new IllegalArgumentException("Mqtt subscribe topicFilter illegal:" + topicFilter); + } + } + } + } + + /** + * 校验 topicName + * + * @param topicName topicName + */ + public static void validateTopicName(String topicName) throws IllegalArgumentException { + if (MqttCodecUtil.isTopicFilter(topicName)) { + throw new IllegalArgumentException("Topic has wildcards char [+] or [#], topicName:" + topicName); + } + } + + /** + * 解析保留消息主题, topicName + * + * @param topicName topicName + */ + public static Pair retainTopicName(String topicName) { + if (topicName.startsWith("$retain/")) { + return getRetainTopicPair(topicName); + } else { + return new Pair<>(topicName, 0); + } + } + + /** + * 处理 $retain topic,注意,时间的三个含义, + * + *

+ * -1: 表示topic有问题需要丢弃消息 + * 0: 表示使用原 topic, + * gt 0:表示保留消息存储时间 + *

+ * + * @param topicName topicName + * @return Pair + */ + private static Pair getRetainTopicPair(String topicName) { + // $retain/ 的长度 + int timeIndexBegin = 8; + int nextLayer = topicName.indexOf(MqttCodecUtil.TOPIC_LAYER, timeIndexBegin); + if (nextLayer == -1) { + return new Pair<>(topicName, -1); + } + int time; + try { + time = Integer.parseInt(topicName.substring(timeIndexBegin, nextLayer)); + } catch (NumberFormatException e) { + time = -1; + } + String retainTopic = topicName.substring(nextLayer + 1); + if (retainTopic.isEmpty()) { + return new Pair<>(topicName, -1); + } else { + return new Pair<>(retainTopic, time); + } + } + + /** + * 判断 topicFilter topicName 是否匹配 + * + * @param topicFilter topicFilter + * @param topicName topicName + * @return 是否匹配 + */ + public static boolean match(String topicFilter, String topicName) { + char[] topicFilterChars = topicFilter.toCharArray(); + char[] topicNameChars = topicName.toCharArray(); + int topicFilterLength = topicFilterChars.length; + int topicNameLength = topicNameChars.length; + int topicFilterIdxEnd = topicFilterLength - 1; + int topicNameIdxEnd = topicNameLength - 1; + char ch; + // 是否进入 + 号层级通配符 + boolean inLayerWildcard = false; + int wildcardCharLen = 0; + topicFilterLoop: + for (int i = 0; i < topicFilterLength; i++) { + ch = topicFilterChars[i]; + if (ch == MqttCodecUtil.TOPIC_WILDCARDS_MORE) { + // 校验: # 通配符只能在最后一位 + if (i < topicFilterIdxEnd) { + throw new IllegalArgumentException("Mqtt subscribe topicFilter illegal:" + topicFilter); + } + return true; + } else if (ch == MqttCodecUtil.TOPIC_WILDCARDS_ONE) { + // 校验: 单独 + 是允许的,判断 + 号前一位是否为 /,如果有后一位也必须为 / + if ((i > 0 && topicFilterChars[i - 1] != MqttCodecUtil.TOPIC_LAYER) || (i < topicFilterIdxEnd && topicFilterChars[i + 1] != MqttCodecUtil.TOPIC_LAYER)) { + throw new IllegalArgumentException("Mqtt subscribe topicFilter illegal:" + topicFilter); + } + // 如果 + 是最后一位,判断 topicName 中是否还存在层级 / + // topicName index + int topicNameIdx = i + wildcardCharLen; + if (i == topicFilterIdxEnd && topicNameLength > topicNameIdx) { + for (int j = topicNameIdx; j < topicNameLength; j++) { + if (topicNameChars[j] == MqttCodecUtil.TOPIC_LAYER) { + return false; + } + } + return true; + } + inLayerWildcard = true; + } else if (ch == MqttCodecUtil.TOPIC_LAYER) { + if (inLayerWildcard) { + inLayerWildcard = false; + } + // 预读下一位,如果是 #,并且 topicName 位数已经不足 + int next = i + 1; + if ((topicFilterLength > next) && topicFilterChars[next] == MqttCodecUtil.TOPIC_WILDCARDS_MORE && topicNameLength < next) { + return true; + } + } + // topicName 长度不够了 + if (topicNameIdxEnd < i) { + return false; + } + // 进入通配符 + if (inLayerWildcard) { + for (int j = i + wildcardCharLen; j < topicNameLength; j++) { + if (topicNameChars[j] == MqttCodecUtil.TOPIC_LAYER) { + wildcardCharLen--; + continue topicFilterLoop; + } else { + wildcardCharLen++; + } + } + } + // topicName index + int topicNameIdx = i + wildcardCharLen; + // topic 已经完成,topicName 还有数据 + if (topicNameIdx > topicNameIdxEnd) { + return false; + } + if (ch != topicNameChars[topicNameIdx]) { + return false; + } + } + // 判断 topicName 是否还有数据 + return topicFilterLength + wildcardCharLen + 1 > topicNameLength; + } + + /** + * 获取处理完成之后的 topic,需要考虑 test/${abc}123 也要替换成 test/+ 而非 test/+123 + * + * @param topicTemplate topic 模板 + * @return 获取处理完成之后的 topic + */ + public static String getTopicFilter(String topicTemplate) { + // 替换 ${name} 为 + + StringTokenizer tokenizer = new StringTokenizer(topicTemplate, TOPIC_LAYER, true); + String token; + StringBuilder topicFilterBuilder = new StringBuilder(topicTemplate.length()); + while (tokenizer.hasMoreTokens()) { + token = tokenizer.nextToken(); + if (TOPIC_LAYER.equals(token)) { + topicFilterBuilder.append(token); + } else if (hasVariable(token)) { + topicFilterBuilder.append(MqttCodecUtil.TOPIC_WILDCARDS_ONE); + } else { + topicFilterBuilder.append(token); + } + } + return topicFilterBuilder.toString(); + } + + /** + * 判断是否含有 ${x} 这样的变量 + * + * @param input input + * @return 是否含有变量 + */ + public static boolean hasVariable(String input) { + if (StrUtil.isBlank(input)) { + return false; + } + int startIndex = input.indexOf("${"); + // 检查是否存在 "${" + if (startIndex == -1) { + return false; + } + int endIndex = input.indexOf('}', startIndex); + // 检查是否同时存在 "${" 和 "}",并且 "}" 在 "${" 之后 + return endIndex != -1 && endIndex > startIndex + 2; + } + + /** + * 解析 topic 中的变量,变量的格式为 ${x},x 为 payload 中的字段名 + * + * @param topicTemplate topic 模板 + * @param payload payload + * @return 解析后的 topic + */ + public static String resolveTopic(String topicTemplate, Object payload) { + if (payload == null) { + return topicTemplate; + } + // 替换变量 + StringBuilder sb = new StringBuilder((int) (topicTemplate.length() * 1.5)); + int cursor = 0; + for (int start, end; (start = topicTemplate.indexOf("${", cursor)) != -1 && (end = topicTemplate.indexOf('}', start)) != -1; ) { + sb.append(topicTemplate, cursor, start); + String fieldName = topicTemplate.substring(start + 2, end); + Object value = getFieldValue(payload, fieldName); + sb.append(value == null ? "" : value); + cursor = end + 1; + } + if (cursor == 0) { + return topicTemplate; + } else { + sb.append(topicTemplate.substring(cursor)); + return sb.toString(); + } + } + + /** + * 获取字段值 + * + * @param obj obj + * @param fieldName fieldName + * @return fieldValue + */ + public static Object getFieldValue(Object obj, String fieldName) { + try { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to resolve field: " + fieldName + " from payload object", e); + } + } + + /** + * 以 / 切分 topic,如果以 / 开头和 / 结尾会多一级,比 split 性能要好 + * + * @param topic topic + * @return part 数组 + */ + public static String[] getTopicParts(String topic) { + // 大部分 topic 层级都在 10 以内 + List tokenList = new ArrayList<>(10); + char[] topicChars = topic.toCharArray(); + int topicLength = topicChars.length; + int topicIdxEnd = topicLength - 1; + char ch; + // 前一个位置 + int prev = 0; + for (int i = 0; i < topicLength; i++) { + ch = topicChars[i]; + if (MqttCodecUtil.TOPIC_LAYER == ch) { + // 如果 / 为起始和最后的位置,添加 / 进 topic part + if (i == 0) { + tokenList.add(TOPIC_LAYER); + prev++; + } else { + tokenList.add(new String(topicChars, prev, i - prev)); + prev = i; + prev++; + if (i == topicIdxEnd) { + tokenList.add(TOPIC_LAYER); + } + } + } else { + if (i == topicIdxEnd) { + tokenList.add(new String(topicChars, prev, topicLength - prev)); + } + } + } + return tokenList.toArray(new String[0]); + } + + /** + * 解析 topic 模板中的变量,不匹配时返回空 Map + * + *

+ * 例如 $SYS/brokers/${node}/clients/${clientid}/disconnected 中提取 node 和 clientid + *

+ * + * @param topicTemplate topicTemplate + * @param topic topic + * @return 获取变量值 + */ + public static Map getTopicVars(String topicTemplate, String topic) { + StrTemplateParser templateParser = new StrTemplateParser(topicTemplate); + return templateParser.getVariables(topic); + } + +} diff --git a/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/timer/AckTimerTask.java b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/timer/AckTimerTask.java new file mode 100644 index 0000000..839152c --- /dev/null +++ b/mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/timer/AckTimerTask.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.util.timer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.utils.timer.Timer; +import org.tio.utils.timer.TimerTask; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * ack TimerTask + * + * @author L.cm + */ +public class AckTimerTask extends TimerTask { + private static final Logger log = LoggerFactory.getLogger(AckTimerTask.class); + + /** + * task + */ + private final Timer timer; + /** + * 需要执行的函数 + */ + private final Runnable command; + /** + * qos 1~2 重试次数 + */ + private final int maxRetryCount; + /** + * 当前自行的次数,默认从第二次开始,因为进重试前已经执行过一次。 + */ + private int count = 1; + + public AckTimerTask(Timer timer, Runnable command, int maxRetryCount, int retryIntervalSecs) { + super(TimeUnit.SECONDS.toMillis(retryIntervalSecs)); + this.timer = Objects.requireNonNull(timer, "Timer is null."); + this.command = Objects.requireNonNull(command, "Runnable command is null."); + this.maxRetryCount = maxRetryCount; + } + + @Override + public void run() { + if (++count <= maxRetryCount + 1) { + // 收先添加任务,保证后续执行 + timer.add(this); + log.debug("Mqtt ack task retry running count:{}.", count); + try { + command.run(); + } catch (Exception e) { + log.error("Mqtt ack task error ", e); + } + } + } + +} diff --git a/mica-mqtt-common/src/main/moditect/module-info.java b/mica-mqtt-common/src/main/moditect/module-info.java new file mode 100644 index 0000000..369a897 --- /dev/null +++ b/mica-mqtt-common/src/main/moditect/module-info.java @@ -0,0 +1,11 @@ +open module org.dromara.mica.mqtt.common { + requires transitive net.dreamlu.mica.net.core; + requires transitive org.dromara.mica.mqtt.codec; + exports org.dromara.mica.mqtt.core.annotation; + exports org.dromara.mica.mqtt.core.common; + exports org.dromara.mica.mqtt.core.deserialize; + exports org.dromara.mica.mqtt.core.function; + exports org.dromara.mica.mqtt.core.serializer; + exports org.dromara.mica.mqtt.core.util; + exports org.dromara.mica.mqtt.core.util.timer; +} diff --git a/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/common/TopicFilterTypeTest.java b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/common/TopicFilterTypeTest.java new file mode 100644 index 0000000..0a3adfe --- /dev/null +++ b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/common/TopicFilterTypeTest.java @@ -0,0 +1,42 @@ +package org.dromara.mica.mqtt.core.common; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * TopicFilterType 测试 + * + * @author L.cm + */ +class TopicFilterTypeTest { + + @Test + void test1() { + String topic1 = "$queue/123"; + TopicFilterType type1 = TopicFilterType.getType(topic1); + Assertions.assertEquals(TopicFilterType.QUEUE, type1); + Assertions.assertTrue(type1.match(topic1, "123")); + Assertions.assertFalse(type1.match(topic1, "/123")); + + String topic2 = "$share/test/123"; + TopicFilterType type2 = TopicFilterType.getType(topic2); + String groupName = TopicFilterType.getShareGroupName(topic2); + Assertions.assertEquals("test", groupName); + Assertions.assertEquals(TopicFilterType.SHARE, type2); + Assertions.assertTrue(type2.match(topic2, "123")); + Assertions.assertFalse(type2.match(topic2, "/123")); + + String topic3 = "$queue//123"; + TopicFilterType type3 = TopicFilterType.getType(topic3); + Assertions.assertEquals(TopicFilterType.QUEUE, type3); + Assertions.assertFalse(type3.match(topic3, "123")); + Assertions.assertTrue(type3.match(topic3, "/123")); + + String topic4 = "$share/test//123"; + TopicFilterType type4 = TopicFilterType.getType(topic4); + Assertions.assertEquals(TopicFilterType.SHARE, type4); + Assertions.assertFalse(type4.match(topic4, "123")); + Assertions.assertTrue(type4.match(topic4, "/123")); + } + +} diff --git a/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/timer/SystemTimerTest.java b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/timer/SystemTimerTest.java new file mode 100644 index 0000000..99de917 --- /dev/null +++ b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/timer/SystemTimerTest.java @@ -0,0 +1,25 @@ +package org.dromara.mica.mqtt.core.timer; + +import org.dromara.mica.mqtt.core.util.timer.AckTimerTask; +import org.tio.utils.timer.SystemTimer; +import org.tio.utils.timer.TimingWheelThread; + +import java.util.concurrent.TimeUnit; + +public class SystemTimerTest { + + public static void main(String[] args) throws InterruptedException { + SystemTimer systemTimer = new SystemTimer("timer"); + + TimingWheelThread timingWheelThread = new TimingWheelThread(systemTimer); + timingWheelThread.start(); + + System.out.println(System.currentTimeMillis()); + systemTimer.add(new AckTimerTask(systemTimer, () -> { + System.out.println("hello!"); + }, 5, 5)); + System.out.println(System.nanoTime()); + + TimeUnit.MINUTES.sleep(10L); + } +} diff --git a/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpCluster.java b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpCluster.java new file mode 100644 index 0000000..098083d --- /dev/null +++ b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpCluster.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.udp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.Node; +import org.tio.core.udp.UdpPacket; +import org.tio.core.udp.UdpServer; +import org.tio.core.udp.task.UdpHandlerRunnable; +import org.tio.core.udp.task.UdpSendRunnable; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.util.concurrent.LinkedBlockingQueue; + +public class UdpCluster { + private static final Logger log = LoggerFactory.getLogger(UdpServer.class); + private final LinkedBlockingQueue handlerQueue = new LinkedBlockingQueue<>(); + private final LinkedBlockingQueue sendQueue = new LinkedBlockingQueue<>(); + private volatile boolean isStopped = false; + private final MulticastSocket multicastSocket; + private final byte[] readBuf; + private final UdpHandlerRunnable udpHandlerRunnable; + private final UdpSendRunnable udpSendRunnable; + private final UdpClusterConfig clusterConfig; + private final InetAddress group; + private final int port; + + public UdpCluster(UdpClusterConfig clusterConfig) throws IOException { + this.clusterConfig = clusterConfig; + this.port = this.clusterConfig.getServerNode().getPort(); + this.multicastSocket = new MulticastSocket(port); + this.readBuf = new byte[this.clusterConfig.getReadBufferSize()]; + this.udpHandlerRunnable = new UdpHandlerRunnable(this.clusterConfig.getUdpHandler(), handlerQueue, multicastSocket); + this.udpSendRunnable = new UdpSendRunnable(sendQueue, this.clusterConfig, multicastSocket); + this.multicastSocket.setSoTimeout(this.clusterConfig.getTimeout()); + this.group = InetAddress.getByName(this.clusterConfig.getServerNode().getIp()); + this.multicastSocket.joinGroup(this.group); + } + + public void send(byte[] data) { + DatagramPacket datagramPacket = new DatagramPacket(data, data.length, this.group, this.port); + this.sendQueue.add(datagramPacket); + } + + public void start() { + startListen(); + startHandler(); + startSend(); + } + + private void startHandler() { + Thread thread = new Thread(udpHandlerRunnable, "tio-udp-server-handler"); + thread.setDaemon(false); + thread.start(); + } + + private void startListen() { + Runnable runnable = () -> { + String startLog = "started tio udp server: " + clusterConfig.getServerNode(); + if (log.isInfoEnabled()) { + log.info(startLog); + } + while (!isStopped) { + try { + DatagramPacket datagramPacket = new DatagramPacket(readBuf, readBuf.length); + multicastSocket.receive(datagramPacket); + byte[] data = new byte[datagramPacket.getLength()]; + System.arraycopy(readBuf, 0, data, 0, datagramPacket.getLength()); + String remoteIp = datagramPacket.getAddress().getHostAddress(); + int remotePort = datagramPacket.getPort(); + Node remote = new Node(remoteIp, remotePort); + UdpPacket udpPacket = new UdpPacket(data, remote); + handlerQueue.put(udpPacket); + } catch (Throwable e) { + log.error(e.toString(), e); + } + } + }; + + Thread thread = new Thread(runnable, "tio-udp-server-listen"); + thread.setDaemon(false); + thread.start(); + } + + private void startSend() { + Thread thread = new Thread(udpSendRunnable, "tio-udp-client-send"); + thread.setDaemon(false); + thread.start(); + } + + public void stop() { + this.isStopped = true; + this.multicastSocket.close(); + this.udpHandlerRunnable.stop(); + } + +} diff --git a/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterConfig.java b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterConfig.java new file mode 100644 index 0000000..91a094b --- /dev/null +++ b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterConfig.java @@ -0,0 +1,32 @@ +package org.dromara.mica.mqtt.core.udp; + +import org.tio.core.Node; +import org.tio.core.udp.UdpConf; +import org.tio.core.udp.intf.UdpHandler; + +public class UdpClusterConfig extends UdpConf { + private UdpHandler udpHandler; + private int readBufferSize = 1024 * 1024; + + public UdpClusterConfig(String ip, int port, UdpHandler udpHandler, int timeout) { + super(timeout); + this.setUdpHandler(udpHandler); + this.setServerNode(new Node(ip, port)); + } + + public int getReadBufferSize() { + return readBufferSize; + } + + public UdpHandler getUdpHandler() { + return udpHandler; + } + + public void setReadBufferSize(int readBufferSize) { + this.readBufferSize = readBufferSize; + } + + public void setUdpHandler(UdpHandler udpHandler) { + this.udpHandler = udpHandler; + } +} diff --git a/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterTest1.java b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterTest1.java new file mode 100644 index 0000000..e4c2f63 --- /dev/null +++ b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterTest1.java @@ -0,0 +1,29 @@ +package org.dromara.mica.mqtt.core.udp; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +/** + * @author L.cm + */ +public class UdpClusterTest1 { + + public static void main(String[] args) throws IOException { + UdpTestHandler udpTestHandler = new UdpTestHandler(); + UdpClusterConfig udpServerConf = new UdpClusterConfig("224.0.0.1", 12345, udpTestHandler, 5000); + UdpCluster udpCluster = new UdpCluster(udpServerConf); + udpCluster.start(); + + byte[] buffer = "hello1".getBytes(); + TimerTask timerTask = new TimerTask() { + @Override + public void run() { + udpCluster.send(buffer); + } + }; + + Timer timer = new Timer(); + timer.schedule(timerTask, 1000, 1000); + } +} diff --git a/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterTest2.java b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterTest2.java new file mode 100644 index 0000000..25bf642 --- /dev/null +++ b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterTest2.java @@ -0,0 +1,31 @@ +package org.dromara.mica.mqtt.core.udp; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +/** + * @author L.cm + */ +public class UdpClusterTest2 { + + public static void main(String[] args) throws IOException { + UdpTestHandler udpTestHandler = new UdpTestHandler(); + UdpClusterConfig udpServerConf = new UdpClusterConfig("224.0.0.1", 12345, udpTestHandler, 5000); + UdpCluster udpCluster = new UdpCluster(udpServerConf); + udpCluster.start(); + + byte[] buffer = "hello2".getBytes(); + TimerTask timerTask = new TimerTask() { + @Override + public void run() { + udpCluster.send(buffer); + } + }; + + Timer timer = new Timer(); + timer.schedule(timerTask, 1000, 1000); + + } + +} diff --git a/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpTestHandler.java b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpTestHandler.java new file mode 100644 index 0000000..e3c2d1d --- /dev/null +++ b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpTestHandler.java @@ -0,0 +1,35 @@ +package org.dromara.mica.mqtt.core.udp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.Node; +import org.tio.core.udp.UdpPacket; +import org.tio.core.udp.intf.UdpHandler; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; + +/** + * @author tanyaowu + */ +public class UdpTestHandler implements UdpHandler { + private static final Logger log = LoggerFactory.getLogger(UdpTestHandler.class); + + public UdpTestHandler() { + } + + @Override + public void handler(UdpPacket udpPacket, DatagramSocket datagramSocket) { + byte[] data = udpPacket.getData(); + String msg = new String(data); + Node remote = udpPacket.getRemote(); + + System.out.printf("收到来自%s的消息:【%s】%n", remote, msg); + DatagramPacket datagramPacket = new DatagramPacket(data, data.length); + try { + datagramSocket.send(datagramPacket); + } catch (Throwable e) { + log.error(e.toString(), e); + } + } +} diff --git a/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/util/TestBean.java b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/util/TestBean.java new file mode 100644 index 0000000..19f2773 --- /dev/null +++ b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/util/TestBean.java @@ -0,0 +1,40 @@ +package org.dromara.mica.mqtt.core.util; + +public class TestBean { + private String name; + private String node; + private String clientId; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNode() { + return node; + } + + public void setNode(String node) { + this.node = node; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + @Override + public String toString() { + return "TestBean{" + + "name='" + name + '\'' + + ", node='" + node + '\'' + + ", clientId='" + clientId + '\'' + + '}'; + } +} diff --git a/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/util/TopicUtilTest.java b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/util/TopicUtilTest.java new file mode 100644 index 0000000..fbc4c17 --- /dev/null +++ b/mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/util/TopicUtilTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.tio.utils.mica.Pair; + +import java.util.Map; + +/** + * TopicUtil 测试 + * + * @author L.cm + */ +class TopicUtilTest { + + @Test + void test() { + // gitee issues #I56BTC /iot/test/# 无法匹配到 /iot/test 和 /iot/test/ + Assertions.assertFalse(TopicUtil.match("+", "/iot/test")); + Assertions.assertFalse(TopicUtil.match("+", "iot/test")); + Assertions.assertFalse(TopicUtil.match("+", "/iot/test")); + Assertions.assertFalse(TopicUtil.match("+", "/iot")); + Assertions.assertFalse(TopicUtil.match("+/test", "/iot/test")); + Assertions.assertFalse(TopicUtil.match("/iot/test/+/", "/iot/test/123")); + + Assertions.assertTrue(TopicUtil.match("/iot/test/+", "/iot/test/123")); + Assertions.assertFalse(TopicUtil.match("/iot/test/+", "/iot/test/123/")); + Assertions.assertTrue(TopicUtil.match("/iot/+/test", "/iot/abc/test")); + Assertions.assertFalse(TopicUtil.match("/iot/+/test", "/iot/abc/test/")); + Assertions.assertFalse(TopicUtil.match("/iot/+/test", "/iot/abc/test1")); + Assertions.assertTrue(TopicUtil.match("/iot/+/+/test", "/iot/abc/123/test")); + Assertions.assertFalse(TopicUtil.match("/iot/+/+/test", "/iot/abc/123/test1")); + Assertions.assertFalse(TopicUtil.match("/iot/+/+/test", "/iot/abc/123/test/")); + Assertions.assertTrue(TopicUtil.match("/iot/+/+/+", "/iot/abc/123/test")); + Assertions.assertFalse(TopicUtil.match("/iot/+/+/+", "/iot/abc/123/test/")); + Assertions.assertTrue(TopicUtil.match("/iot/+/test", "/iot/a/test")); + Assertions.assertTrue(TopicUtil.match("/iot/+/test", "/iot/a/test")); + Assertions.assertFalse(TopicUtil.match("/iot/+/+/+", "/iot/a//test/")); + Assertions.assertFalse(TopicUtil.match("/iot/+/+/+", "/iot/a/b/c/")); + Assertions.assertFalse(TopicUtil.match("/iot/+/+/+", "/iot/a")); + + Assertions.assertTrue(TopicUtil.match("#", "/iot/test")); + Assertions.assertTrue(TopicUtil.match("/iot/test/#", "/iot/test")); + Assertions.assertTrue(TopicUtil.match("/iot/test/#", "/iot/test/")); + Assertions.assertTrue(TopicUtil.match("/iot/test/#", "/iot/test/1")); + Assertions.assertTrue(TopicUtil.match("/iot/test/#", "/iot/test/123123/12312")); + + Assertions.assertTrue(TopicUtil.match("/iot/test/123", "/iot/test/123")); + } + + @Test + void test2() { + String s1 = "$SYS/brokers/${node}/clients/${clientId}/disconnected"; + String s2 = "$SYS/brokers/+/clients/+/disconnected"; + String s3 = TopicUtil.getTopicFilter(s1); + Assertions.assertEquals(s2, s3); + s1 = "$SYS/brokers/${node}/clients/${clientId}abc/disconnected"; + s3 = TopicUtil.getTopicFilter(s1); + Assertions.assertEquals(s2, s3); + s1 = "$SYS/brokers/${node}/clients/${clientId}abc${x}/disconnected"; + s3 = TopicUtil.getTopicFilter(s1); + Assertions.assertEquals(s2, s3); + s1 = "$SYS/brokers/${node}/clients/abc${clientId}abc${x}123/disconnected"; + s3 = TopicUtil.getTopicFilter(s1); + Assertions.assertEquals(s2, s3); + } + + @Test + void test3() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + TopicUtil.validateTopicFilter("/iot/test/+a"); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + TopicUtil.validateTopicFilter("/iot/test/a+"); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + TopicUtil.validateTopicFilter("/iot/test/+a/"); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + TopicUtil.validateTopicFilter("/iot/test/a+/"); + }); + Assertions.assertDoesNotThrow(() -> TopicUtil.validateTopicFilter("+")); + Assertions.assertDoesNotThrow(() -> TopicUtil.validateTopicFilter("/iot/test/+")); + Assertions.assertDoesNotThrow(() -> TopicUtil.validateTopicFilter("/iot/test/+/")); + } + + @Test + void test4() { + String test1 = "Hello, ${name}!"; + String test2 = "No variable here"; + String test3 = "Invalid ${variable"; + String test4 = "${name}!"; + Assertions.assertTrue(TopicUtil.hasVariable(test1)); + Assertions.assertFalse(TopicUtil.hasVariable(test2)); + Assertions.assertFalse(TopicUtil.hasVariable(test3)); + Assertions.assertTrue(TopicUtil.hasVariable(test4)); + } + + @Test + void testResolveTopic() { + String message = "Hello, ${name}!"; + TestBean testBean = new TestBean(); + testBean.setName("张三"); + String m1 = TopicUtil.resolveTopic(message, testBean); + Assertions.assertEquals("Hello, 张三!", m1); + String s1 = "$SYS/brokers/${node}/clients/${clientId}/disconnected"; + testBean.setNode("node1"); + testBean.setClientId("abc123"); + String m2 = TopicUtil.resolveTopic(s1, testBean); + Assertions.assertEquals("$SYS/brokers/node1/clients/abc123/disconnected", m2); + String m3 = TopicUtil.resolveTopic("/iot/test/123", testBean); + Assertions.assertEquals("/iot/test/123", m3); + } + + @Test + void testRetainTopicName() { + Pair pair1 = TopicUtil.retainTopicName("$retain/15/x/y"); + Assertions.assertEquals("x/y", pair1.getLeft()); + Pair pair2 = TopicUtil.retainTopicName("$retain/15//x/y"); + Assertions.assertEquals("/x/y", pair2.getLeft()); + Pair pair3 = TopicUtil.retainTopicName("$retain/15/"); + Assertions.assertEquals(-1, pair3.getRight()); + Pair pair4 = TopicUtil.retainTopicName("$retain/"); + Assertions.assertEquals(-1, pair4.getRight()); + } + + @Test + void testGetTopicVars() { + // 测试匹配 + String s1 = "$SYS/brokers/${node}/clients/${clientId}/disconnected"; + String s2 = "$SYS/brokers/node1/clients/test1/disconnected"; + Map vars = TopicUtil.getTopicVars(s1, s2); + Assertions.assertEquals("node1", vars.get("node")); + Assertions.assertEquals("test1", vars.get("clientId")); + // 测试不匹配 + String s3 = "$SYS/brokers/${node}/clients/${clientId}/disconnected"; + String s4 = "abc/brokers/node1/clients/test1/disconnected"; + Map vars1 = TopicUtil.getTopicVars(s3, s4); + // 不匹配会返回空 + Assertions.assertTrue(vars1.isEmpty()); + } + +} diff --git a/mica-mqtt-server/README.md b/mica-mqtt-server/README.md new file mode 100644 index 0000000..c12f2ff --- /dev/null +++ b/mica-mqtt-server/README.md @@ -0,0 +1,92 @@ +# 使用文档 + +## 添加依赖 + +```xml + + org.dromara.mica-mqtt + mica-mqtt-server + ${mica-mqtt.version} + +``` + +## 服务端使用 + +```java +// 注意:为了能接受更多链接(降低内存),请添加 jvm 参数 -Xss129k +MqttServer mqttServer = MqttServer.create() + // 服务端 ip 默认为空,0.0.0.0,建议不要设置 + .ip("0.0.0.0") + // 默认:1883 + .port(1883) + // 默认为: 8092(mqtt 默认最大消息大小),为了降低内存可以减小小此参数,如果消息过大 t-io 会尝试解析多次(建议根据实际业务情况而定) + .readBufferSize(512) + // 最大包体长度,如果包体过大需要设置此参数,默认为: 8092 + .maxBytesInMessage(1024 * 100) + // 自定义认证 + .authHandler((clientId, userName, password) -> true) + // 消息监听 + .messageListener((context, clientId, topic, qos, message) -> { + logger.info("clientId:{} payload:{}", clientId, new String(message.payload(), StandardCharsets.UTF_8)); + }) + // 心跳超时时间,默认:120s + .heartbeatTimeout(120_1000L) + // ssl 配置 + .useSsl("", "", "") + // 开启代理协议,支持 nginx 开启 tcp proxy_protocol on; 时转发源 ip 信息。2.4.1 版本开始支持 + .proxyProtocolEnable() + // 自定义客户端上下线监听 + .connectStatusListener(new IMqttConnectStatusListener() { + @Override + public void online(String clientId) { + + } + + @Override + public void offline(String clientId) { + + } + }) + // 自定义消息转发,可用 mq 广播实现集群化处理 + .messageDispatcher(new IMqttMessageDispatcher() { + @Override + public void config(MqttServer mqttServer) { + + } + + @Override + public boolean send(Message message) { + return false; + } + + @Override + public boolean send(String clientId, Message message) { + return false; + } + }) + .debug() // 开启 debug 信息日志 + .start(); + +// 发送给某个客户端 +mqttServer.publish("clientId","/test/123", "mica最牛皮".getBytes(StandardCharsets.UTF_8)); + +// 发送给所有在线监听这个 topic 的客户端 +mqttServer.publishAll("/test/123", "mica最牛皮".getBytes(StandardCharsets.UTF_8)); + +// 停止服务 +mqttServer.stop(); +``` + +## http 和 websocket 依赖(2.4.2或之前版本需要该步骤): + +开启 http 或 websocket 需要添加 mica-net-http 依赖,如果不需要 http、websocket 把它们可以使用 `.httpEnable(false)` 和 `.websocketEnable(false)` 关掉就不需要该依赖了。 + +```xml + + net.dreamlu + mica-net-http + ${version} + +``` + +另外 http api 需要项目带有 jackson、fastjson、fastjson2、gson、hutool-json、snack3(mica-mqtt 2.3.4开始支持) 这些json工具其一。 diff --git a/mica-mqtt-server/pom.xml b/mica-mqtt-server/pom.xml new file mode 100644 index 0000000..107bb59 --- /dev/null +++ b/mica-mqtt-server/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + mica-mqtt + ${revision} + + mica-mqtt-server + ${project.artifactId} + https://mica-mqtt.dreamlu.net/guide/java/server.html + + + + org.dromara.mica-mqtt + mica-mqtt-common + + + net.dreamlu + mica-net-http + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.tinylog + slf4j-tinylog + test + + + org.tinylog + tinylog-impl + test + + + + org.openjdk.jol + jol-core + 0.17 + test + + + + + + + org.moditect + moditect-maven-plugin + + + + + diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttConst.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttConst.java new file mode 100644 index 0000000..f990fe9 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttConst.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server; + +/** + * 常量 + * + * @author L.cm + */ +public interface MqttConst { + + /** + * session 有效期,小于等于 0,关闭时清理,大于 0 采用缓存处理 + */ + String SESSION_EXPIRES = "session_expires"; + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttMessageInterceptors.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttMessageInterceptors.java new file mode 100644 index 0000000..0e8cf2f --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttMessageInterceptors.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server; + +import org.dromara.mica.mqtt.core.server.interceptor.IMqttMessageInterceptor; +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.tio.core.ChannelContext; + +import java.util.ArrayList; +import java.util.List; + +/** + * mqtt 消息拦截器集合 + * + * @author L.cm + */ +public class MqttMessageInterceptors { + private final List interceptors; + + public MqttMessageInterceptors() { + this.interceptors = new ArrayList<>(); + } + + /** + * 添加拦截器 + * + * @param interceptor IMqttMessageInterceptor + */ + void add(IMqttMessageInterceptor interceptor) { + this.interceptors.add(interceptor); + } + + /** + * 建链后触发本方法,注:建链不一定成功,需要关注参数isConnected + * + * @param context ChannelContext + * @param isConnected 是否连接成功,true:表示连接成功,false:表示连接失败 + * @param isReconnect 是否是重连, true: 表示这是重新连接,false: 表示这是第一次连接 + * @throws Exception Exception + */ + public void onAfterConnected(ChannelContext context, boolean isConnected, boolean isReconnect) throws Exception { + for (IMqttMessageInterceptor interceptor : interceptors) { + interceptor.onAfterConnected(context, isConnected, isReconnect); + } + } + + /** + * 接收到TCP层传过来的数据后 + * + * @param context ChannelContext + * @param receivedBytes 本次接收了多少字节 + * @throws Exception Exception + */ + public void onAfterReceivedBytes(ChannelContext context, int receivedBytes) throws Exception { + for (IMqttMessageInterceptor interceptor : interceptors) { + interceptor.onAfterReceivedBytes(context, receivedBytes); + } + } + + /** + * 解码成功后触发本方法 + * + * @param context ChannelContext + * @param message MqttMessage + * @param packetSize packetSize + * @throws Exception Exception + */ + public void onAfterDecoded(ChannelContext context, MqttMessage message, int packetSize) throws Exception { + for (IMqttMessageInterceptor interceptor : interceptors) { + interceptor.onAfterDecoded(context, message, packetSize); + } + } + + /** + * 处理一个消息包后 + * + * @param context ChannelContext + * @param message MqttMessage + * @param cost 本次处理消息耗时,单位:毫秒 + * @throws Exception Exception + */ + public void onAfterHandled(ChannelContext context, MqttMessage message, long cost) throws Exception { + for (IMqttMessageInterceptor interceptor : interceptors) { + interceptor.onAfterHandled(context, message, cost); + } + } + + /** + * 处理一个消息包后 + * + * @param context ChannelContext + * @param message MqttMessage + * @param isSentSuccess 是否发送成功 + * @throws Exception Exception + */ + public void onAfterSent(ChannelContext context, MqttMessage message, boolean isSentSuccess) throws Exception { + for (IMqttMessageInterceptor interceptor : interceptors) { + interceptor.onAfterSent(context, message, isSentSuccess); + } + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServer.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServer.java new file mode 100644 index 0000000..1b8cc89 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServer.java @@ -0,0 +1,564 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.common.MqttPendingPublish; +import org.dromara.mica.mqtt.core.serializer.MqttSerializer; +import org.dromara.mica.mqtt.core.server.enums.MessageType; +import org.dromara.mica.mqtt.core.server.listener.MqttProtocolListeners; +import org.dromara.mica.mqtt.core.server.model.ClientInfo; +import org.dromara.mica.mqtt.core.server.model.Message; +import org.dromara.mica.mqtt.core.server.model.Subscribe; +import org.dromara.mica.mqtt.core.server.session.IMqttSessionManager; +import org.dromara.mica.mqtt.core.server.store.IMqttMessageStore; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; +import org.tio.core.TioConfig; +import org.tio.core.stat.vo.StatVo; +import org.tio.server.TioServerConfig; +import org.tio.server.task.ServerHeartbeatTask; +import org.tio.utils.hutool.StrUtil; +import org.tio.utils.mica.Pair; +import org.tio.utils.page.Page; +import org.tio.utils.page.PageUtils; +import org.tio.utils.timer.TimerTask; +import org.tio.utils.timer.TimerTaskService; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * mqtt 服务端 + * + * @author L.cm + * @author ChangJin Wei (魏昌进) + */ +public final class MqttServer { + private static final Logger logger = LoggerFactory.getLogger(MqttServer.class); + private final MqttServerCreator serverCreator; + private final TioServerConfig serverConfig; + private final TimerTaskService taskService; + private final MqttProtocolListeners listeners; + private final IMqttSessionManager sessionManager; + private final IMqttMessageStore messageStore; + private final MqttSerializer mqttSerializer; + + MqttServer(MqttServerCreator serverCreator, + TioServerConfig serverConfig, + MqttProtocolListeners listeners) { + this.serverCreator = serverCreator; + this.serverConfig = serverConfig; + this.taskService = serverConfig.getTaskService(); + this.listeners = listeners; + this.sessionManager = serverCreator.getSessionManager(); + this.messageStore = serverCreator.getMessageStore(); + this.mqttSerializer = serverCreator.getMqttSerializer(); + } + + public static MqttServerCreator create() { + return new MqttServerCreator(); + } + + /** + * 获取 ServerTioConfig + * + * @return the serverTioConfig + */ + public TioServerConfig getServerConfig() { + return this.serverConfig; + } + + /** + * 获取 mqtt 配置 + * + * @return MqttServerCreator + */ + public MqttServerCreator getServerCreator() { + return serverCreator; + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload) { + return publish(clientId, topic, payload, MqttQoS.QOS0); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload, MqttQoS qos) { + return publish(clientId, topic, payload, qos, false); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload, boolean retain) { + return publish(clientId, topic, payload, MqttQoS.QOS0, retain); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload, MqttQoS qos, boolean retain) { + // 校验 topic + TopicUtil.validateTopicName(topic); + // 存储保留消息 + if (retain) { + Pair retainPair = TopicUtil.retainTopicName(topic); + int timeOut = retainPair.getRight(); + if (timeOut < 0) { + logger.error("MqttPublishMessage topic {} 不符合 $retain/${ttl}/topic 规则.", topic); + return false; + } + topic = retainPair.getLeft(); + this.saveRetainMessage(topic, timeOut, qos, payload); + } + // 获取 context + ChannelContext context = Tio.getByBsId(getServerConfig(), clientId); + if (context == null || context.isClosed()) { + logger.warn("Mqtt Topic:{} publish to clientId:{} ChannelContext is null may be disconnected.", topic, clientId); + return false; + } + Byte subMqttQoS = sessionManager.searchSubscribe(topic, clientId); + if (subMqttQoS == null) { + logger.warn("Mqtt Topic:{} publish but clientId:{} not subscribed.", topic, clientId); + return false; + } + MqttQoS mqttQoS = qos.value() > subMqttQoS ? MqttQoS.valueOf(subMqttQoS) : qos; + return publish(context, clientId, topic, payload, mqttQoS, retain); + } + + /** + * 直接发布消息 + * + * @param context ChannelContext + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(ChannelContext context, String clientId, String topic, Object payload, MqttQoS qos, boolean retain) { + boolean isHighLevelQoS = MqttQoS.QOS1 == qos || MqttQoS.QOS2 == qos; + int messageId = isHighLevelQoS ? sessionManager.getPacketId(clientId) : -1; + byte[] newPayload = payload instanceof byte[] ? (byte[]) payload : mqttSerializer.serialize(payload); + MqttPublishMessage message = MqttPublishMessage.builder() + .topicName(topic) + .payload(newPayload) + .qos(qos) + .retained(retain) + .messageId(messageId) + .build(); + // 先启动高 qos 的重试 + if (isHighLevelQoS) { + MqttPendingPublish pendingPublish = new MqttPendingPublish(message, qos); + sessionManager.addPendingPublish(clientId, messageId, pendingPublish); + pendingPublish.startPublishRetransmissionTimer(taskService, context); + } + // 发送消息 + boolean result = Tio.send(context, message); + logger.debug("MQTT Topic:{} qos:{} retain:{} publish clientId:{} result:{}", topic, qos, retain, clientId, result); + return result; + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload) { + return publishAll(topic, payload, MqttQoS.QOS0); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload, MqttQoS qos) { + return publishAll(topic, payload, qos, false); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload, boolean retain) { + return publishAll(topic, payload, MqttQoS.QOS0, retain); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload, MqttQoS qos, boolean retain) { + // 校验 topic + TopicUtil.validateTopicName(topic); + // 存储保留消息 + if (retain) { + Pair retainPair = TopicUtil.retainTopicName(topic); + int timeOut = retainPair.getRight(); + if (timeOut < 0) { + logger.error("MqttPublishMessage topic {} 不符合 $retain/${ttl}/topic 规则.", topic); + return false; + } + topic = retainPair.getLeft(); + this.saveRetainMessage(topic, timeOut, qos, payload); + } + // 查找订阅该 topic 的客户端 + List subscribeList = sessionManager.searchSubscribe(topic); + if (subscribeList.isEmpty()) { + logger.debug("Mqtt Topic:{} publishAll but subscribe client list is empty.", topic); + return false; + } + for (Subscribe subscribe : subscribeList) { + String clientId = subscribe.getClientId(); + ChannelContext context = Tio.getByBsId(getServerConfig(), clientId); + if (context == null || context.isClosed()) { + logger.warn("Mqtt Topic:{} publish to clientId:{} channel is null may be disconnected.", topic, clientId); + continue; + } + int subMqttQoS = subscribe.getMqttQoS(); + MqttQoS mqttQoS = qos.value() > subMqttQoS ? MqttQoS.valueOf(subMqttQoS) : qos; + publish(context, clientId, topic, payload, mqttQoS, false); + } + return true; + } + + /** + * 发送消息到客户端 + * + * @param topic topic + * @param message Message + * @return 是否成功 + */ + public boolean sendToClient(String topic, Message message) { + // 客户端id + String clientId = message.getClientId(); + MqttQoS mqttQoS = MqttQoS.valueOf(message.getQos()); + if (StrUtil.isBlank(clientId)) { + return publishAll(topic, message.getPayload(), mqttQoS, message.isRetain()); + } else { + return publish(clientId, topic, message.getPayload(), mqttQoS, message.isRetain()); + } + } + + /** + * 存储保留消息 + * + * @param topic topic + * @param mqttQoS MqttQoS + * @param payload ByteBuffer + */ + private void saveRetainMessage(String topic, int timeout, MqttQoS mqttQoS, Object payload) { + Message retainMessage = new Message(); + retainMessage.setTopic(topic); + retainMessage.setQos(mqttQoS.value()); + retainMessage.setPayload(payload instanceof byte[] ? (byte[]) payload : mqttSerializer.serialize(payload)); + retainMessage.setMessageType(MessageType.DOWN_STREAM); + // 将保留消息标记成 false,避免后续下发时再次存储 + retainMessage.setRetain(false); + retainMessage.setDup(false); + retainMessage.setTimestamp(System.currentTimeMillis()); + retainMessage.setNode(serverCreator.getNodeName()); + this.messageStore.addRetainMessage(topic, timeout, retainMessage); + } + + /** + * 获取客户端信息 + * + * @param clientId clientId + * @return ClientInfo + */ + public ClientInfo getClientInfo(String clientId) { + ChannelContext context = Tio.getByBsId(this.getServerConfig(), clientId); + if (context == null) { + return null; + } + return ClientInfo.form(serverCreator, context, ClientInfo::new); + } + + /** + * 获取客户端信息 + * + * @param context ChannelContext + * @return ClientInfo + */ + public ClientInfo getClientInfo(ChannelContext context) { + return ClientInfo.form(serverCreator, context, ClientInfo::new); + } + + /** + * 获取所有的客户端 + * + * @return 客户端列表 + */ + public List getClients() { + return getClients(this.serverCreator, this.getServerConfig()); + } + + /** + * 分页获取所有的客户端 + * + * @param serverCreator MqttServerCreator + * @param tioConfig TioConfig + * @return 客户端列表 + */ + public static List getClients(MqttServerCreator serverCreator, TioConfig tioConfig) { + return Tio.getAll(tioConfig) + .stream() + .map(context -> ClientInfo.form(serverCreator, context, ClientInfo::new)) + .collect(Collectors.toList()); + } + + /** + * 分页获取所有的客户端 + * + * @param pageIndex pageIndex,默认为 1 + * @param pageSize pageSize,默认为所有 + * @return 分页 + */ + public Page getClients(Integer pageIndex, Integer pageSize) { + return getClients(this.serverCreator, this.getServerConfig(), pageIndex, pageSize); + } + + /** + * 分页获取所有的客户端 + * + * @param serverCreator MqttServerCreator + * @param tioConfig TioConfig + * @param pageIndex pageIndex,默认为 1 + * @param pageSize pageSize,默认为所有 + * @return 分页 + */ + public static Page getClients(MqttServerCreator serverCreator, TioConfig tioConfig, Integer pageIndex, Integer pageSize) { + return PageUtils.fromSet(Tio.getAll(tioConfig), pageIndex, pageSize, context -> ClientInfo.form(serverCreator, context, ClientInfo::new)); + } + + /** + * 获取统计数据 + * + * @return StatVo + */ + public StatVo getStat() { + return serverConfig.getStat(); + } + + /** + * 获取客户端订阅情况 + * + * @param clientId clientId + * @return 订阅集合 + */ + public List getSubscriptions(String clientId) { + return serverCreator.getSessionManager().getSubscriptions(clientId); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay) { + return schedule(command, delay, null); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay, Executor executor) { + return this.taskService.addTask((systemTimer -> new TimerTask(delay) { + @Override + public void run() { + try { + // 1. 再次添加 任务 + systemTimer.add(this); + // 2. 执行任务 + if (executor == null) { + command.run(); + } else { + executor.execute(command); + } + } catch (Exception e) { + logger.error("Mqtt server schedule error", e); + } + } + })); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay) { + return scheduleOnce(command, delay, null); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay, Executor executor) { + return this.taskService.addTask((systemTimer -> new TimerTask(delay) { + @Override + public void run() { + try { + if (executor == null) { + command.run(); + } else { + executor.execute(command); + } + } catch (Exception e) { + logger.error("Mqtt server schedule once error", e); + } + } + })); + } + + /** + * 获取 ChannelContext + * + * @param clientId clientId + * @return ChannelContext + */ + public ChannelContext getChannelContext(String clientId) { + return Tio.getByBsId(getServerConfig(), clientId); + } + + /** + * 服务端主动断开连接 + * + * @param clientId clientId + */ + public void close(String clientId) { + Tio.remove(getChannelContext(clientId), "Mqtt server close this connects."); + } + + /** + * 启动服务 + * + * @return 是否启动 + */ + public boolean start() { + // 1. 启动 task + this.taskService.start(); + // 2. 启动心跳检测 + this.taskService.addTask(systemTimer -> new ServerHeartbeatTask(systemTimer, serverConfig)); + // 3. 启动监听器 + listeners.start(); + return true; + } + + /** + * 停止服务 + * + * @return 是否停止 + */ + public boolean stop() { + // 停止服务 + boolean result = listeners.stop(); + // 停止工作线程 + ExecutorService mqttExecutor = serverCreator.getMqttExecutor(); + try { + mqttExecutor.shutdown(); + } catch (Exception e1) { + logger.error(e1.getMessage(), e1); + } + try { + // 等待线程池中的任务结束,等待 10 分钟 + result &= mqttExecutor.awaitTermination(10, TimeUnit.MINUTES); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error(e.getMessage(), e); + } + try { + sessionManager.clean(); + } catch (Throwable e) { + logger.error("MqttServer stop session clean error.", e); + } + try { + messageStore.clean(); + } catch (Throwable e) { + logger.error("MqttServer stop message store clean error.", e); + } + return result; + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerAioHandler.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerAioHandler.java new file mode 100644 index 0000000..15f6cf8 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerAioHandler.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server; + +import org.dromara.mica.mqtt.codec.MqttDecoder; +import org.dromara.mica.mqtt.codec.MqttEncoder; +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.codes.MqttConnectReasonCode; +import org.dromara.mica.mqtt.codec.exception.DecoderException; +import org.dromara.mica.mqtt.codec.exception.MqttIdentifierRejectedException; +import org.dromara.mica.mqtt.codec.exception.MqttUnacceptableProtocolVersionException; +import org.dromara.mica.mqtt.codec.message.*; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdVariableHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; +import org.tio.core.TioConfig; +import org.tio.core.exception.TioDecodeException; +import org.tio.core.intf.Packet; +import org.tio.server.intf.TioServerHandler; + +import java.nio.ByteBuffer; + +/** + * @author L.cm + */ +public class MqttServerAioHandler implements TioServerHandler { + private static final Logger log = LoggerFactory.getLogger(MqttServerAioHandler.class); + private final MqttDecoder mqttDecoder; + private final MqttEncoder mqttEncoder; + private final MqttServerProcessor processor; + + public MqttServerAioHandler(MqttServerCreator serverCreator, MqttServerProcessor processor) { + this.mqttDecoder = new MqttDecoder(serverCreator.getMaxBytesInMessage(), serverCreator.getMaxClientIdLength()); + this.mqttEncoder = MqttEncoder.INSTANCE; + this.processor = processor; + } + + /** + * 根据ByteBuffer解码成业务需要的Packet对象. + * 如果收到的数据不全,导致解码失败,请返回null,在下次消息来时框架层会自动续上前面的收到的数据 + * + * @param buffer 参与本次希望解码的ByteBuffer + * @param limit ByteBuffer的limit + * @param position ByteBuffer的position,不一定是0哦 + * @param readableLength ByteBuffer参与本次解码的有效数据(= limit - position) + * @param context ChannelContext + * @return Packet + */ + @Override + public Packet decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext context) throws TioDecodeException { + try { + return mqttDecoder.doDecode(context, buffer, readableLength); + } catch (DecoderException e) { + processFailure(context, e); + throw new TioDecodeException(e); + } + } + + /** + * 编码 + * + * @param packet Packet + * @param tioConfig TioConfig + * @param context ChannelContext + * @return ByteBuffer + */ + @Override + public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext context) { + return mqttEncoder.doEncode(context, (MqttMessage) packet); + } + + /** + * 处理消息包 + * + * @param packet Packet + * @param context ChannelContext + */ + @Override + public void handler(Packet packet, ChannelContext context) { + MqttMessage mqttMessage = (MqttMessage) packet; + log.debug("MqttMessage:{}", mqttMessage); + MqttFixedHeader fixedHeader = mqttMessage.fixedHeader(); + MqttMessageType messageType = fixedHeader.messageType(); + // 2. 单独处理 CONNECT 的消息 + if (MqttMessageType.CONNECT == messageType) { + processor.processConnect(context, (MqttConnectMessage) mqttMessage); + return; + } + // 3. 判定是否认证成功 + if (!context.isAccepted()) { + Tio.remove(context, "Mqtt connected but is not accepted."); + return; + } + // 4. 按类型的消息处理 + switch (messageType) { + case PUBLISH: + processor.processPublish(context, (MqttPublishMessage) mqttMessage); + break; + case PUBACK: + processor.processPubAck(context, (MqttMessageIdVariableHeader) mqttMessage.variableHeader()); + break; + case PUBREC: + processor.processPubRec(context, (MqttMessageIdVariableHeader) mqttMessage.variableHeader()); + break; + case PUBREL: + processor.processPubRel(context, (MqttMessageIdVariableHeader) mqttMessage.variableHeader()); + break; + case PUBCOMP: + processor.processPubComp(context, (MqttMessageIdVariableHeader) mqttMessage.variableHeader()); + break; + case SUBSCRIBE: + processor.processSubscribe(context, (MqttSubscribeMessage) mqttMessage); + break; + case UNSUBSCRIBE: + processor.processUnSubscribe(context, (MqttUnSubscribeMessage) mqttMessage); + break; + case PINGREQ: + processor.processPingReq(context); + break; + case DISCONNECT: + processor.processDisConnect(context); + break; + default: + break; + } + } + + /** + * 处理失败 + * + * @param context ChannelContext + * @param cause DecoderException + */ + private void processFailure(ChannelContext context, DecoderException cause) { + if (cause instanceof MqttUnacceptableProtocolVersionException) { + // 不支持的协议版本 + MqttConnAckMessage message = MqttConnAckMessage.builder() + .returnCode(MqttConnectReasonCode.CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION) + .sessionPresent(false) + .build(); + Tio.bSend(context, message); + } else if (cause instanceof MqttIdentifierRejectedException) { + // 不合格的 clientId + MqttConnAckMessage message = MqttConnAckMessage.builder() + .returnCode(MqttConnectReasonCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED) + .sessionPresent(false) + .build(); + Tio.bSend(context, message); + } + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerAioListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerAioListener.java new file mode 100644 index 0000000..cf295cb --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerAioListener.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server; + +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.core.server.dispatcher.IMqttMessageDispatcher; +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.dromara.mica.mqtt.core.server.model.Message; +import org.dromara.mica.mqtt.core.server.session.IMqttSessionManager; +import org.dromara.mica.mqtt.core.server.store.IMqttMessageStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.intf.Packet; +import org.tio.server.DefaultTioServerListener; +import org.tio.utils.hutool.StrUtil; + +import java.io.IOException; + +/** + * mqtt 服务监听 + * + * @author L.cm + */ +public class MqttServerAioListener extends DefaultTioServerListener { + private static final Logger logger = LoggerFactory.getLogger(MqttServerAioListener.class); + private final IMqttMessageStore messageStore; + private final IMqttSessionManager sessionManager; + private final IMqttMessageDispatcher messageDispatcher; + private final IMqttConnectStatusListener connectStatusListener; + private final MqttMessageInterceptors messageInterceptors; + + public MqttServerAioListener(MqttServerCreator serverCreator) { + this.messageStore = serverCreator.getMessageStore(); + this.sessionManager = serverCreator.getSessionManager(); + this.messageDispatcher = serverCreator.getMessageDispatcher(); + this.connectStatusListener = serverCreator.getConnectStatusListener(); + this.messageInterceptors = serverCreator.getMessageInterceptors(); + } + + @Override + public boolean onHeartbeatTimeout(ChannelContext context, long interval, int heartbeatTimeoutCount) { + String clientId = context.getBsId(); + logger.info("Mqtt HeartbeatTimeout clientId:{} interval:{} count:{}", clientId, interval, heartbeatTimeoutCount); + return false; + } + + @Override + public void onAfterConnected(ChannelContext context, boolean isConnected, boolean isReconnect) throws Exception { + messageInterceptors.onAfterConnected(context, isConnected, isReconnect); + } + + @Override + public void onBeforeClose(ChannelContext context, Throwable throwable, String remark, boolean isRemove) { + // 标记认证为 false + context.setAccepted(false); + // 1. 业务 id + String clientId = context.getBsId(); + // 2. 判断是否正常断开 + boolean isNotNormalDisconnect = !context.isBizStatus(); + context.setBizStatus(false); + if (isNotNormalDisconnect || throwable != null) { + // 避免网络异常时短期照成大量异常打印,会导致内存突增 + if (throwable instanceof IOException) { + logger.error("Mqtt server close clientId:{}, remark:{} isRemove:{} error:{}", clientId, remark, isRemove, throwable.getMessage()); + } else { + logger.error("Mqtt server close clientId:{}, remark:{} isRemove:{}", clientId, remark, isRemove, throwable); + } + } else { + logger.info("Mqtt server close clientId:{} remark:{} isRemove:{}", clientId, remark, isRemove); + } + // 3. 业务 id 不能为空 + if (StrUtil.isBlank(clientId)) { + return; + } + // 4. 对于异常断开连接,处理遗嘱消息 + if (isNotNormalDisconnect) { + sendWillMessage(clientId); + } + // 5. 会话清理 + cleanSession(clientId); + // 6. 下线事件 + notify(context, clientId, remark); + } + + private void sendWillMessage(String clientId) { + // 发送遗嘱消息 + try { + Message willMessage = messageStore.getWillMessage(clientId); + if (willMessage == null) { + return; + } + boolean result = messageDispatcher.send(willMessage); + logger.debug("Mqtt server clientId:{} send willMessage result:{}.", clientId, result); + // 4. 清理遗嘱消息 + messageStore.clearWillMessage(clientId); + } catch (Throwable throwable) { + logger.error("Mqtt server clientId:{} send willMessage error.", clientId, throwable); + } + } + + private void cleanSession(String clientId) { + try { + sessionManager.remove(clientId); + } catch (Throwable throwable) { + logger.error("Mqtt server clientId:{} session clean error.", clientId, throwable); + } + } + + private void notify(ChannelContext context, String clientId, String remark) { + String username = context.getUserId(); + try { + connectStatusListener.offline(context, clientId, username, remark); + } catch (Throwable throwable) { + logger.error("Mqtt server clientId:{} offline notify error.", clientId, throwable); + } + } + + @Override + public void onAfterSent(ChannelContext context, Packet packet, boolean isSentSuccess) throws Exception { + if (packet instanceof MqttMessage) { + messageInterceptors.onAfterSent(context, (MqttMessage) packet, isSentSuccess); + } + } + + @Override + public void onAfterReceivedBytes(ChannelContext context, int receivedBytes) throws Exception { + messageInterceptors.onAfterReceivedBytes(context, receivedBytes); + } + + @Override + public void onAfterDecoded(ChannelContext context, Packet packet, int packetSize) throws Exception { + if (packet instanceof MqttMessage) { + messageInterceptors.onAfterDecoded(context, (MqttMessage) packet, packetSize); + } + } + + @Override + public void onAfterHandled(ChannelContext context, Packet packet, long cost) throws Exception { + if (packet instanceof MqttMessage) { + messageInterceptors.onAfterHandled(context, (MqttMessage) packet, cost); + } + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerCreator.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerCreator.java new file mode 100644 index 0000000..b441e4b --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerCreator.java @@ -0,0 +1,590 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server; + +import org.dromara.mica.mqtt.codec.MqttConstant; +import org.dromara.mica.mqtt.core.serializer.MqttJsonSerializer; +import org.dromara.mica.mqtt.core.serializer.MqttSerializer; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerAuthHandler; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerPublishPermission; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerSubscribeValidator; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerUniqueIdService; +import org.dromara.mica.mqtt.core.server.broker.DefaultMqttBrokerDispatcher; +import org.dromara.mica.mqtt.core.server.dispatcher.AbstractMqttMessageDispatcher; +import org.dromara.mica.mqtt.core.server.dispatcher.IMqttMessageDispatcher; +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.dromara.mica.mqtt.core.server.event.IMqttMessageListener; +import org.dromara.mica.mqtt.core.server.event.IMqttSessionListener; +import org.dromara.mica.mqtt.core.server.interceptor.IMqttMessageInterceptor; +import org.dromara.mica.mqtt.core.server.listener.IMqttProtocolListener; +import org.dromara.mica.mqtt.core.server.listener.MqttHttpApiListener; +import org.dromara.mica.mqtt.core.server.listener.MqttProtocolListener; +import org.dromara.mica.mqtt.core.server.listener.MqttProtocolListeners; +import org.dromara.mica.mqtt.core.server.session.IMqttSessionManager; +import org.dromara.mica.mqtt.core.server.session.InMemoryMqttSessionManager; +import org.dromara.mica.mqtt.core.server.store.IMqttMessageStore; +import org.dromara.mica.mqtt.core.server.store.InMemoryMqttMessageStore; +import org.dromara.mica.mqtt.core.server.support.DefaultMqttConnectStatusListener; +import org.dromara.mica.mqtt.core.server.support.DefaultMqttServerAuthHandler; +import org.dromara.mica.mqtt.core.server.support.DefaultMqttServerProcessor; +import org.dromara.mica.mqtt.core.server.support.DefaultMqttServerUniqueIdServiceImpl; +import org.tio.core.Node; +import org.tio.core.task.HeartbeatMode; +import org.tio.server.TioServerConfig; +import org.tio.server.intf.TioServerHandler; +import org.tio.server.intf.TioServerListener; +import org.tio.utils.hutool.StrUtil; +import org.tio.utils.json.JsonAdapter; +import org.tio.utils.json.JsonUtil; +import org.tio.utils.thread.ThreadUtils; +import org.tio.utils.timer.DefaultTimerTaskService; +import org.tio.utils.timer.TimerTaskService; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * mqtt 服务端参数构造 + * + * @author L.cm + * @author ChangJin Wei (魏昌进) + */ +public class MqttServerCreator { + + /** + * 名称 + */ + private String name = "Mica-Mqtt-Server"; + /** + * 监听器 + */ + private final List listeners = new ArrayList<>(); + /** + * 心跳超时时间(单位: 毫秒 默认: 1000 * 120),如果用户不希望框架层面做心跳相关工作,请把此值设为0或负数 + */ + private Long heartbeatTimeout; + /** + * MQTT 客户端 keepalive 系数,连接超时缺省为连接设置的 keepalive * keepaliveBackoff * 2,默认:0.75 + *

+ * 如果读者想对该值做一些调整,可以在此进行配置。比如设置为 0.75,则变为 keepalive * 1.5。但是该值不得小于 0.5,否则将小于 keepalive 设定的时间。 + */ + private float keepaliveBackoff = 0.75F; + /** + * 接收数据的 buffer size,默认:8k + */ + private int readBufferSize = MqttConstant.DEFAULT_MAX_READ_BUFFER_SIZE; + /** + * 消息解析最大 bytes 长度,默认:10M + */ + private int maxBytesInMessage = MqttConstant.DEFAULT_MAX_BYTES_IN_MESSAGE; + /** + * 认证处理器 + */ + private IMqttServerAuthHandler authHandler; + /** + * 唯一 id 服务 + */ + private IMqttServerUniqueIdService uniqueIdService; + /** + * 订阅校验器 + */ + private IMqttServerSubscribeValidator subscribeValidator; + /** + * 发布权限校验 + */ + private IMqttServerPublishPermission publishPermission; + /** + * 消息处理器 + */ + private IMqttMessageDispatcher messageDispatcher; + /** + * 消息存储 + */ + private IMqttMessageStore messageStore; + /** + * session 管理 + */ + private IMqttSessionManager sessionManager; + /** + * session 监听 + */ + private IMqttSessionListener sessionListener; + /** + * 消息监听 + */ + private IMqttMessageListener messageListener; + /** + * 连接状态监听 + */ + private IMqttConnectStatusListener connectStatusListener; + /** + * debug + */ + private boolean debug = false; + /** + * mqtt 3.1 会校验此参数为 23,为了减少问题设置成了 64 + */ + private int maxClientIdLength = MqttConstant.DEFAULT_MAX_CLIENT_ID_LENGTH; + /** + * 节点名称,用于处理集群 + */ + private String nodeName; + /** + * 是否用队列发送 + */ + private boolean useQueueSend = true; + /** + * 是否用队列解码(系统初始化时确定该值,中途不要变更此值,否则在切换的时候可能导致消息丢失) + */ + private boolean useQueueDecode = false; + /** + * 是否开启监控,不开启可节省内存,默认:true + */ + private boolean statEnable = true; + /** + * TioConfig 自定义配置 + */ + private Consumer tioConfigCustomize; + /** + * 消息拦截器 + */ + private final MqttMessageInterceptors messageInterceptors = new MqttMessageInterceptors(); + /** + * taskService + */ + private TimerTaskService taskService; + /** + * 业务消费线程 + */ + private ExecutorService mqttExecutor; + /** + * json 处理器 + */ + private JsonAdapter jsonAdapter; + /** + * 开启代理协议支持 + */ + private boolean proxyProtocolOn = false; + + private MqttSerializer mqttSerializer; + + public String getName() { + return name; + } + + public MqttServerCreator name(String name) { + this.name = name; + return this; + } + + + public Long getHeartbeatTimeout() { + return heartbeatTimeout; + } + + public MqttServerCreator heartbeatTimeout(Long heartbeatTimeout) { + this.heartbeatTimeout = heartbeatTimeout; + return this; + } + + public float getKeepaliveBackoff() { + return keepaliveBackoff; + } + + public MqttServerCreator keepaliveBackoff(float keepaliveBackoff) { + if (keepaliveBackoff <= 0.5) { + throw new IllegalArgumentException("keepalive backoff must greater than 0.5"); + } + this.keepaliveBackoff = keepaliveBackoff; + return this; + } + + public int getReadBufferSize() { + return readBufferSize; + } + + public MqttServerCreator readBufferSize(int readBufferSize) { + this.readBufferSize = readBufferSize; + return this; + } + + public int getMaxBytesInMessage() { + return maxBytesInMessage; + } + + public MqttServerCreator maxBytesInMessage(int maxBytesInMessage) { + if (maxBytesInMessage < 1) { + throw new IllegalArgumentException("maxBytesInMessage must be greater than 0."); + } + this.maxBytesInMessage = maxBytesInMessage; + return this; + } + + public IMqttServerAuthHandler getAuthHandler() { + return authHandler; + } + + public MqttServerCreator authHandler(IMqttServerAuthHandler authHandler) { + this.authHandler = authHandler; + return this; + } + + public MqttServerCreator usernamePassword(String username, String password) { + return authHandler(new DefaultMqttServerAuthHandler(username, password)); + } + + public IMqttServerUniqueIdService getUniqueIdService() { + return uniqueIdService; + } + + public MqttServerCreator uniqueIdService(IMqttServerUniqueIdService uniqueIdService) { + this.uniqueIdService = uniqueIdService; + return this; + } + + public IMqttServerSubscribeValidator getSubscribeValidator() { + return subscribeValidator; + } + + public MqttServerCreator subscribeValidator(IMqttServerSubscribeValidator subscribeValidator) { + this.subscribeValidator = subscribeValidator; + return this; + } + + public IMqttServerPublishPermission getPublishPermission() { + return publishPermission; + } + + public MqttServerCreator publishPermission(IMqttServerPublishPermission publishPermission) { + this.publishPermission = publishPermission; + return this; + } + + public IMqttMessageDispatcher getMessageDispatcher() { + return messageDispatcher; + } + + public MqttServerCreator messageDispatcher(IMqttMessageDispatcher messageDispatcher) { + this.messageDispatcher = messageDispatcher; + return this; + } + + public IMqttMessageStore getMessageStore() { + return messageStore; + } + + public MqttServerCreator messageStore(IMqttMessageStore messageStore) { + this.messageStore = messageStore; + return this; + } + + public IMqttSessionManager getSessionManager() { + return sessionManager; + } + + public MqttServerCreator sessionManager(IMqttSessionManager sessionManager) { + this.sessionManager = sessionManager; + return this; + } + + public IMqttSessionListener getSessionListener() { + return sessionListener; + } + + public MqttServerCreator sessionListener(IMqttSessionListener sessionListener) { + this.sessionListener = sessionListener; + return this; + } + + public IMqttMessageListener getMessageListener() { + return messageListener; + } + + public MqttServerCreator messageListener(IMqttMessageListener messageListener) { + this.messageListener = messageListener; + return this; + } + + public IMqttConnectStatusListener getConnectStatusListener() { + return connectStatusListener; + } + + public MqttServerCreator connectStatusListener(IMqttConnectStatusListener connectStatusListener) { + this.connectStatusListener = connectStatusListener; + return this; + } + + public boolean isDebug() { + return debug; + } + + public MqttServerCreator debug() { + this.debug = true; + return this; + } + + public int getMaxClientIdLength() { + return maxClientIdLength; + } + + public MqttServerCreator maxClientIdLength(int maxClientIdLength) { + this.maxClientIdLength = maxClientIdLength; + return this; + } + + public String getNodeName() { + return nodeName; + } + + public MqttServerCreator nodeName(String nodeName) { + this.nodeName = nodeName; + return this; + } + + public boolean isUseQueueSend() { + return useQueueSend; + } + + public MqttServerCreator useQueueSend(boolean useQueueSend) { + this.useQueueSend = useQueueSend; + return this; + } + + public boolean isUseQueueDecode() { + return useQueueDecode; + } + + public MqttServerCreator useQueueDecode(boolean useQueueDecode) { + this.useQueueDecode = useQueueDecode; + return this; + } + + public boolean isStatEnable() { + return statEnable; + } + + public MqttServerCreator statEnable() { + return statEnable(true); + } + + public MqttServerCreator statEnable(boolean enable) { + this.statEnable = enable; + return this; + } + + public MqttServerCreator tioConfigCustomize(Consumer tioConfigCustomize) { + this.tioConfigCustomize = tioConfigCustomize; + return this; + } + + public MqttMessageInterceptors getMessageInterceptors() { + return messageInterceptors; + } + + public MqttServerCreator addInterceptor(IMqttMessageInterceptor interceptor) { + this.messageInterceptors.add(interceptor); + return this; + } + + public MqttServerCreator taskService(TimerTaskService taskService) { + this.taskService = taskService; + return this; + } + + public ExecutorService getMqttExecutor() { + return mqttExecutor; + } + + public MqttServerCreator mqttExecutor(ExecutorService mqttExecutor) { + this.mqttExecutor = mqttExecutor; + return this; + } + + public JsonAdapter getJsonAdapter() { + return jsonAdapter; + } + + public MqttServerCreator jsonAdapter(JsonAdapter jsonAdapter) { + this.jsonAdapter = JsonUtil.getJsonAdapter(jsonAdapter); + return this; + } + + public boolean isProxyProtocolEnabled() { + return proxyProtocolOn; + } + + public MqttServerCreator proxyProtocolEnable() { + return proxyProtocolEnable(true); + } + + public MqttServerCreator proxyProtocolEnable(boolean proxyProtocolOn) { + this.proxyProtocolOn = proxyProtocolOn; + return this; + } + + public MqttSerializer getMqttSerializer() { + return mqttSerializer; + } + + public MqttServerCreator mqttSerializer(MqttSerializer mqttSerializer) { + this.mqttSerializer = mqttSerializer; + return this; + } + + public MqttServerCreator enableMqtt() { + return enableMqtt(MqttProtocolListener.Builder::build); + } + + public MqttServerCreator enableMqtt(int port) { + return enableMqtt(builder -> builder.serverNode(port).build()); + } + + public MqttServerCreator enableMqtt(Function function) { + return addMqttProtocolListener(function.apply(MqttProtocolListener.mqttBuilder())); + } + + public MqttServerCreator enableMqttSsl(Function function) { + return addMqttProtocolListener(function.apply(MqttProtocolListener.mqttSslBuilder())); + } + + public MqttServerCreator enableMqttWs() { + return enableMqttWs(MqttProtocolListener.Builder::build); + } + + public MqttServerCreator enableMqttWs(int port) { + return enableMqttWs(builder -> builder.serverNode(port).build()); + } + + public MqttServerCreator enableMqttWs(Function function) { + return addMqttProtocolListener(function.apply(MqttProtocolListener.wsBuilder())); + } + + public MqttServerCreator enableMqttWss(Function function) { + return addMqttProtocolListener(function.apply(MqttProtocolListener.wssBuilder())); + } + + public MqttServerCreator enableMqttHttpApi() { + return enableMqttHttpApi(MqttHttpApiListener.Builder::build); + } + + public MqttServerCreator enableMqttHttpApi(int port) { + return enableMqttHttpApi(builder -> builder.serverNode(port).build()); + } + + public MqttServerCreator enableMqttHttpApi(Function function) { + return addMqttProtocolListener(function.apply(MqttHttpApiListener.builder())); + } + + private MqttServerCreator addMqttProtocolListener(IMqttProtocolListener listener) { + boolean contains = this.listeners.contains(listener); + if (contains) { + String protocolName = listener.getProtocol().name(); + Node serverNode = listener.getServerNode(); + throw new IllegalStateException("Mqtt protocol:" + protocolName + " serverNode:" + serverNode + " already exists"); + } + this.listeners.add(listener); + return this; + } + + public MqttServer build() { + // 默认的节点名称,用于集群 + if (StrUtil.isBlank(this.nodeName)) { + this.nodeName = StrUtil.getNanoId(); + } + if (this.uniqueIdService == null) { + this.uniqueIdService = new DefaultMqttServerUniqueIdServiceImpl(); + } + if (this.messageDispatcher == null) { + this.messageDispatcher = new DefaultMqttBrokerDispatcher(); + } + if (this.sessionManager == null) { + this.sessionManager = new InMemoryMqttSessionManager(); + } + if (this.messageStore == null) { + this.messageStore = new InMemoryMqttMessageStore(); + } + if (this.connectStatusListener == null) { + this.connectStatusListener = new DefaultMqttConnectStatusListener(); + } + // taskService + if (this.taskService == null) { + this.taskService = new DefaultTimerTaskService(200L, 60); + } + // 业务线程池 + if (this.mqttExecutor == null) { + this.mqttExecutor = ThreadUtils.getBizExecutor(ThreadUtils.MAX_POOL_SIZE_FOR_TIO); + } + // 序列化 + if (this.mqttSerializer == null) { + this.mqttSerializer = new MqttJsonSerializer(); + } + // 监听器为空,开启默认的 mqtt server + if (this.listeners.isEmpty()) { + this.enableMqtt(); + } + // AckService + DefaultMqttServerProcessor serverProcessor = new DefaultMqttServerProcessor(this, this.taskService, mqttExecutor); + // 1. 处理消息 + TioServerHandler handler = new MqttServerAioHandler(this, serverProcessor); + // 2. t-io 监听 + TioServerListener listener = new MqttServerAioListener(this); + // 3. t-io 配置 + TioServerConfig tioConfig = new TioServerConfig(this.name, handler, listener); + tioConfig.setUseQueueDecode(this.useQueueDecode); + tioConfig.setUseQueueSend(this.useQueueSend); + tioConfig.setTaskService(this.taskService); + tioConfig.statOn = this.statEnable; + // 4. mqtt 消息最大长度,小于 1 则使用默认的,可通过 property tio.default.read.buffer.size 设置默认大小 + if (this.readBufferSize > 0) { + tioConfig.setReadBufferSize(this.readBufferSize); + } + // 5. 是否开启代理协议 + tioConfig.enableProxyProtocol(this.proxyProtocolOn); + // 6. 设置 t-io 心跳 timeout + if (this.heartbeatTimeout != null && this.heartbeatTimeout > 0) { + tioConfig.setHeartbeatTimeout(this.heartbeatTimeout); + } + tioConfig.setHeartbeatBackoff(this.keepaliveBackoff); + tioConfig.setHeartbeatMode(HeartbeatMode.LAST_RESP); + if (this.debug) { + tioConfig.debug = true; + } + // 自定义处理 + if (this.tioConfigCustomize != null) { + this.tioConfigCustomize.accept(tioConfig); + } + // 配置 json + this.jsonAdapter(JsonUtil.getJsonAdapter(getJsonAdapter())); + // MqttServer + MqttProtocolListeners listeners = new MqttProtocolListeners(this, tioConfig, this.listeners); + MqttServer mqttServer = new MqttServer(this, tioConfig, listeners); + // 如果是默认的消息转发器,设置 mqttServer + if (this.messageDispatcher instanceof AbstractMqttMessageDispatcher) { + ((AbstractMqttMessageDispatcher) this.messageDispatcher).config(mqttServer); + } + return mqttServer; + } + + public MqttServer start() { + MqttServer mqttServer = this.build(); + mqttServer.start(); + return mqttServer; + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerCustomizer.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerCustomizer.java new file mode 100644 index 0000000..7ea83e7 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerCustomizer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server; + +/** + * MqttServer 配置自定义 + * + * @author L.cm + */ +@FunctionalInterface +public interface MqttServerCustomizer { + + /** + * MqttServerCreator 自定义扩展 + * + * @param creator MqttServerCreator + */ + void customize(MqttServerCreator creator); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerProcessor.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerProcessor.java new file mode 100644 index 0000000..ff0f62b --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerProcessor.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server; + +import org.dromara.mica.mqtt.codec.message.MqttConnectMessage; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.message.MqttSubscribeMessage; +import org.dromara.mica.mqtt.codec.message.MqttUnSubscribeMessage; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdVariableHeader; +import org.tio.core.ChannelContext; + +/** + * mqtt broker 处理器 + * + * @author L.cm + */ +public interface MqttServerProcessor { + + /** + * 处理链接 + * + * @param context ChannelContext + * @param message MqttConnectMessage + */ + void processConnect(ChannelContext context, MqttConnectMessage message); + + /** + * Publish + * + * @param context ChannelContext + * @param message MqttPublishMessage + */ + void processPublish(ChannelContext context, MqttPublishMessage message); + + /** + * PubAck + * + * @param context ChannelContext + * @param variableHeader MqttMessageIdVariableHeader + */ + void processPubAck(ChannelContext context, MqttMessageIdVariableHeader variableHeader); + + /** + * PubRec + * + * @param context ChannelContext + * @param variableHeader MqttMessageIdVariableHeader + */ + void processPubRec(ChannelContext context, MqttMessageIdVariableHeader variableHeader); + + /** + * PubRel + * + * @param context ChannelContext + * @param variableHeader MqttMessageIdVariableHeader + */ + void processPubRel(ChannelContext context, MqttMessageIdVariableHeader variableHeader); + + /** + * PubComp + * + * @param context ChannelContext + * @param variableHeader MqttMessageIdVariableHeader + */ + void processPubComp(ChannelContext context, MqttMessageIdVariableHeader variableHeader); + + /** + * 监听 + * + * @param context ChannelContext + * @param message MqttSubscribeMessage + */ + void processSubscribe(ChannelContext context, MqttSubscribeMessage message); + + /** + * 取消监听 + * + * @param context ChannelContext + * @param message MqttUnsubscribeMessage + */ + void processUnSubscribe(ChannelContext context, MqttUnSubscribeMessage message); + + /** + * ping 消息处理 + * + * @param context ChannelContext + */ + void processPingReq(ChannelContext context); + + /** + * 断开连接 + * + * @param context ChannelContext + */ + void processDisConnect(ChannelContext context); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerAuthHandler.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerAuthHandler.java new file mode 100644 index 0000000..efd3e1a --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerAuthHandler.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.auth; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +/** + * mqtt 服务端,认证处理器 + * + * @author L.cm + */ +@FunctionalInterface +public interface IMqttServerAuthHandler { + Logger logger = LoggerFactory.getLogger(IMqttServerAuthHandler.class); + + /** + * 认证 + * + * @param context ChannelContext + * @param uniqueId mqtt 内唯一id,默认和 clientId 相同 + * @param clientId 客户端 ID + * @param username 用户名 + * @param password 密码 + * @return 是否认证成功 + */ + default boolean verifyAuthenticate(ChannelContext context, String uniqueId, String clientId, String username, String password) { + try { + return authenticate(context, uniqueId, clientId, username, password); + } catch (Throwable e) { + logger.error("Mqtt client node:{} authenticate error uniqueId:{} clientId:{} username:{} password:{}", context.getClientNode(), uniqueId, clientId, username, password, e); + return false; + } + } + + /** + * 认证 + * + * @param context ChannelContext + * @param uniqueId mqtt 内唯一id,默认和 clientId 相同 + * @param clientId 客户端 ID + * @param username 用户名 + * @param password 密码 + * @return 是否认证成功 + */ + boolean authenticate(ChannelContext context, String uniqueId, String clientId, String username, String password); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerPublishPermission.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerPublishPermission.java new file mode 100644 index 0000000..1a2afb3 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerPublishPermission.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.auth; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +/** + * mqtt 服务端校验客户端是否有发布权限 + * + * @author L.cm + */ +public interface IMqttServerPublishPermission { + Logger logger = LoggerFactory.getLogger(IMqttServerPublishPermission.class); + + /** + * 否有发布权限 + * + * @param context ChannelContext + * @param clientId 客户端 id + * @param topic topic + * @param qoS MqttQoS + * @param isRetain 是否保留消息 + * @return 否有发布权限 + */ + default boolean verifyPermission(ChannelContext context, String clientId, String topic, MqttQoS qoS, boolean isRetain) { + try { + return hasPermission(context, clientId, topic, qoS, isRetain); + } catch (Throwable e) { + logger.error("Mqtt client node:{} publish permission error clientId:{} username:{} topic:{} qos:{}", context.getClientNode(), clientId, context.getUserId(), topic, qoS, e); + return false; + } + } + + /** + * 否有发布权限 + * + * @param context ChannelContext + * @param clientId 客户端 id + * @param topic topic + * @param qoS MqttQoS + * @param isRetain 是否保留消息 + * @return 否有发布权限 + */ + boolean hasPermission(ChannelContext context, String clientId, String topic, MqttQoS qoS, boolean isRetain); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerSubscribeValidator.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerSubscribeValidator.java new file mode 100644 index 0000000..3289377 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerSubscribeValidator.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.auth; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +/** + * mqtt 服务端,认证处理器 + * + * @author L.cm + */ +public interface IMqttServerSubscribeValidator { + Logger logger = LoggerFactory.getLogger(IMqttServerSubscribeValidator.class); + + /** + * 校验订阅的 topicFilter + * + * @param context ChannelContext + * @param clientId clientId + * @param topicFilter topicFilter + * @param qoS MqttQoS + * @return 是否有权限 + */ + default boolean verifyTopicFilter(ChannelContext context, String clientId, String topicFilter, MqttQoS qoS) { + try { + return isValid(context, clientId, topicFilter, qoS); + } catch (Throwable e) { + logger.error("Mqtt client node:{} subscribe error clientId:{} username:{} topicFilter:{} qos:{}", context.getClientNode(), clientId, context.getUserId(), topicFilter, qoS, e); + return false; + } + } + + /** + * 是否可以订阅 + * + * @param context ChannelContext + * @param clientId 客户端 id + * @param topicFilter 订阅 topic + * @param qoS MqttQoS + * @return 是否可以订阅 + */ + boolean isValid(ChannelContext context, String clientId, String topicFilter, MqttQoS qoS); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerUniqueIdService.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerUniqueIdService.java new file mode 100644 index 0000000..3e3426a --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerUniqueIdService.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.auth; + +import org.tio.core.ChannelContext; + +/** + * mqtt 服务端唯一 id 绑定 + * + * @author L.cm + */ +public interface IMqttServerUniqueIdService { + + /** + * 获取 mqtt 唯一id,用来绑定 mqtt 内的 session 等功能 + * + * @param context ChannelContext + * @param clientId clientId + * @param userName userName + * @param password password + * @return uniqueId + */ + String getUniqueId(ChannelContext context, String clientId, String userName, String password); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/broker/DefaultMqttBrokerDispatcher.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/broker/DefaultMqttBrokerDispatcher.java new file mode 100644 index 0000000..517c647 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/broker/DefaultMqttBrokerDispatcher.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.broker; + +import org.dromara.mica.mqtt.core.server.dispatcher.AbstractMqttMessageDispatcher; +import org.dromara.mica.mqtt.core.server.model.Message; + +/** + * 默认的消息转发器 + * + * @author L.cm + */ +public class DefaultMqttBrokerDispatcher extends AbstractMqttMessageDispatcher { + + @Override + public void sendAll(Message message) { + + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/cluster/MqttClusterMessageListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/cluster/MqttClusterMessageListener.java new file mode 100644 index 0000000..3cdc115 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/cluster/MqttClusterMessageListener.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.cluster; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.core.server.enums.MessageType; +import org.dromara.mica.mqtt.core.server.event.IMqttMessageListener; +import org.dromara.mica.mqtt.core.server.model.Message; +import org.dromara.mica.mqtt.core.server.session.IMqttSessionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; +import org.tio.server.ServerChannelContext; + +/** + * mqtt 集群消息处理 + * + * @author L.cm + */ +public class MqttClusterMessageListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClusterMessageListener.class); + private final String nodeName; + private final IMqttMessageListener messageListener; + private final IMqttSessionManager sessionManager; + private final MqttServer mqttServer; + + public MqttClusterMessageListener(MqttServer mqttServer) { + this.nodeName = mqttServer.getServerCreator().getNodeName(); + this.messageListener = mqttServer.getServerCreator().getMessageListener(); + this.sessionManager = mqttServer.getServerCreator().getSessionManager(); + this.mqttServer = mqttServer; + } + + /** + * 来着集群的消息 + * + * @param message Message + */ + public void onMessage(Message message) { + MessageType messageType = message.getMessageType(); + String topic = message.getTopic(); + if (MessageType.CONNECT == messageType) { + // 1. 如果一个 clientId 在集群多个服务上连接时断开其他的 + String node = message.getNode(); + if (nodeName.equals(node)) { + return; + } + String clientId = message.getClientId(); + ChannelContext context = Tio.getByBsId(mqttServer.getServerConfig(), clientId); + if (context != null) { + Tio.remove(context, "clientId:[" + clientId + "] now bind on mqtt node:" + node); + } + } else if (MessageType.SUBSCRIBE == messageType) { + // http api 订阅广播 + String formClientId = message.getFromClientId(); + ChannelContext context = mqttServer.getChannelContext(formClientId); + if (context != null) { + sessionManager.addSubscribe(topic, formClientId, message.getQos()); + } + } else if (MessageType.UNSUBSCRIBE == messageType) { + // http api 取消订阅广播 + String formClientId = message.getFromClientId(); + ChannelContext context = mqttServer.getChannelContext(formClientId); + if (context != null) { + sessionManager.removeSubscribe(topic, formClientId); + } + } else if (MessageType.UP_STREAM == messageType) { + // mqtt 上行消息,需要发送到对应的监听的客户端 + mqttServer.sendToClient(topic, message); + } else if (MessageType.DOWN_STREAM == messageType) { + // http rest api 下行消息也会转发到此 + mqttServer.sendToClient(topic, message); + } else if (MessageType.HTTP_API == messageType) { + // http rest api 消息也会转发到此 + MqttQoS mqttQoS = MqttQoS.valueOf(message.getQos()); + mqttServer.publishAll(topic, message.getPayload(), mqttQoS, message.isRetain()); + // 触发消息 + try { + onHttpApiMessage(topic, mqttQoS, message); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + } else if (MessageType.DISCONNECT == messageType) { + String clientId = message.getClientId(); + ChannelContext context = mqttServer.getChannelContext(clientId); + if (context != null) { + Tio.remove(context, "Mqtt server delete clients:" + clientId); + } + } + } + + private void onHttpApiMessage(String topic, MqttQoS mqttQoS, Message message) { + String clientId = message.getClientId(); + // 构造 context + ServerChannelContext context = new ServerChannelContext(mqttServer.getServerConfig()); +// Node serverNode = mqttServer.getTioServer().getServerNode(); +// context.setServerNode(serverNode); +// Node clientNode = mqttServer.getWebServer().getServerNode(); +// context.setClientNode(clientNode); + context.setBsId(clientId); + context.setUserId(MessageType.HTTP_API.name()); + // 构造 MqttPublishMessage + MqttPublishMessage publishMessage = MqttPublishMessage.builder() + .topicName(topic) + .qos(mqttQoS) + .retained(message.isRetain()) + .payload(message.getPayload()) + .build(); + messageListener.onMessage(context, clientId, topic, mqttQoS, publishMessage); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/dispatcher/AbstractMqttMessageDispatcher.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/dispatcher/AbstractMqttMessageDispatcher.java new file mode 100644 index 0000000..f6ac7c9 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/dispatcher/AbstractMqttMessageDispatcher.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.dispatcher; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.core.server.enums.MessageType; +import org.dromara.mica.mqtt.core.server.event.IMqttMessageListener; +import org.dromara.mica.mqtt.core.server.model.Message; +import org.dromara.mica.mqtt.core.server.session.IMqttSessionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; +import org.tio.server.ServerChannelContext; + +import java.util.Objects; + +/** + * 内部消息转发抽象 + * + * @author L.cm + */ +public abstract class AbstractMqttMessageDispatcher implements IMqttMessageDispatcher { + private static final Logger logger = LoggerFactory.getLogger(AbstractMqttMessageDispatcher.class); + protected MqttServer mqttServer; + protected IMqttMessageListener messageListener; + protected IMqttSessionManager sessionManager; + + public void config(MqttServer mqttServer) { + this.mqttServer = mqttServer; + this.messageListener = mqttServer.getServerCreator().getMessageListener(); + this.sessionManager = mqttServer.getServerCreator().getSessionManager(); + } + + /** + * 转发所有消息 + * + * @param message Message + */ + public abstract void sendAll(Message message); + + @Override + public boolean send(Message message) { + Objects.requireNonNull(mqttServer, "MqttServer require not Null."); + // 1. 先发送到本服务 + MessageType messageType = message.getMessageType(); + if (MessageType.SUBSCRIBE == messageType) { + sessionManager.addSubscribe(message.getTopic(), message.getFromClientId(), message.getQos()); + } else if (MessageType.UNSUBSCRIBE == messageType) { + sessionManager.removeSubscribe(message.getTopic(), message.getFromClientId()); + } else if (MessageType.UP_STREAM == messageType) { + mqttServer.sendToClient(message.getTopic(), message); + } else if (MessageType.DOWN_STREAM == messageType) { + mqttServer.sendToClient(message.getTopic(), message); + } else if (MessageType.HTTP_API == messageType) { + String topic = message.getTopic(); + // http rest api 消息也会转发到此 + MqttQoS mqttQoS = MqttQoS.valueOf(message.getQos()); + mqttServer.publishAll(topic, message.getPayload(), mqttQoS, message.isRetain()); + // 触发消息 + try { + onHttpApiMessage(topic, mqttQoS, message); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + } else if (MessageType.DISCONNECT == messageType) { + String clientId = message.getClientId(); + ChannelContext context = mqttServer.getChannelContext(clientId); + if (context != null) { + Tio.remove(context, "Mqtt server delete clients:" + clientId); + } + } + sendAll(message); + return true; + } + + @Override + public void sendRetainMessage(ChannelContext context, String clientId, Message retainMessage) { + String topic = retainMessage.getTopic(); + byte[] payload = retainMessage.getPayload(); + MqttQoS mqttQoS = MqttQoS.valueOf(retainMessage.getQos()); + boolean retain = retainMessage.isRetain(); + mqttServer.publish(context, clientId, topic, payload, mqttQoS, retain); + } + + private void onHttpApiMessage(String topic, MqttQoS mqttQoS, Message message) { + String clientId = message.getClientId(); + // 构造 context + ServerChannelContext context = new ServerChannelContext(mqttServer.getServerConfig()); +// Node serverNode = mqttServer.getTioServer().getServerNode(); +// context.setServerNode(serverNode); +// Node clientNode = mqttServer.getWebServer().getServerNode(); +// context.setClientNode(clientNode); + context.setBsId(clientId); + context.setUserId(MessageType.HTTP_API.name()); + // 构造 MqttPublishMessage + MqttPublishMessage publishMessage = MqttPublishMessage.builder() + .topicName(topic) + .qos(mqttQoS) + .retained(message.isRetain()) + .payload(message.getPayload()) + .build(); + messageListener.onMessage(context, clientId, topic, mqttQoS, publishMessage); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/dispatcher/IMqttMessageDispatcher.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/dispatcher/IMqttMessageDispatcher.java new file mode 100644 index 0000000..36dbb69 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/dispatcher/IMqttMessageDispatcher.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.dispatcher; + +import org.dromara.mica.mqtt.core.server.model.Message; +import org.tio.core.ChannelContext; + +/** + * mqtt 消息调度器 + * + * @author L.cm + */ +public interface IMqttMessageDispatcher { + + /** + * 发送消息 + * + * @param message 消息 + * @return 是否成功 + */ + boolean send(Message message); + + /** + * 订阅时下发保留消息,直接发布到订阅的连接 + * + * @param context ChannelContext + * @param clientId clientId + * @param retainMessage retainMessage + */ + void sendRetainMessage(ChannelContext context, String clientId, Message retainMessage); +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/enums/MessageType.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/enums/MessageType.java new file mode 100644 index 0000000..57f3d60 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/enums/MessageType.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.enums; + +/** + * 消息类型 + * + * @author L.cm + */ +public enum MessageType { + + /** + * 连接 + */ + CONNECT(1), + /** + * 主题订阅 + */ + SUBSCRIBE(2), + /** + * 取消订阅 + */ + UNSUBSCRIBE(3), + /** + * 上行数据 + */ + UP_STREAM(4), + /** + * 下行数据 + */ + DOWN_STREAM(5), + /** + * 断开连接 + */ + DISCONNECT(6), + /** + * http api 上下行消息 + */ + HTTP_API(7), + ; + + private static final MessageType[] VALUES; + + static { + // this prevent values to be assigned with the wrong order + // and ensure valueOf to work fine + final MessageType[] values = values(); + VALUES = new MessageType[values.length + 1]; + for (MessageType mqttMessageType : values) { + final int value = mqttMessageType.value; + if (VALUES[value] != null) { + throw new AssertionError("value already in use: " + value); + } + VALUES[value] = mqttMessageType; + } + } + + private final int value; + + MessageType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static MessageType valueOf(int type) { + if (type <= 0 || type >= VALUES.length) { + throw new IllegalArgumentException("unknown message type: " + type); + } + return VALUES[type]; + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttConnectStatusListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttConnectStatusListener.java new file mode 100644 index 0000000..5bb6625 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttConnectStatusListener.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.event; + +import org.tio.core.ChannelContext; + +/** + * mqtt 链接状态事件 + * + * @author L.cm + */ +public interface IMqttConnectStatusListener { + + /** + * 设备上线(连接成功) + * + * @param context ChannelContext + * @param clientId clientId + * @param username username + */ + void online(ChannelContext context, String clientId, String username); + + /** + * 设备离线 + * + * @param context ChannelContext + * @param clientId clientId + * @param username username + * @param reason reason + */ + void offline(ChannelContext context, String clientId, String username, String reason); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttMessageListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttMessageListener.java new file mode 100644 index 0000000..3c59564 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttMessageListener.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.event; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.tio.core.ChannelContext; + +/** + * mqtt 消息处理 + * + * @author L.cm + */ +@FunctionalInterface +public interface IMqttMessageListener { + + /** + * 监听到消息 + * + * @param context ChannelContext + * @param clientId clientId + * @param topic topic + * @param qoS MqttQoS + * @param message Message + */ + void onMessage(ChannelContext context, String clientId, String topic, MqttQoS qoS, MqttPublishMessage message); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttSessionListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttSessionListener.java new file mode 100644 index 0000000..12d5de7 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttSessionListener.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.event; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.tio.core.ChannelContext; + +/** + * mqtt session 事件 + * + * @author L.cm + */ +public interface IMqttSessionListener { + + /** + * 订阅 + * + * @param context ChannelContext + * @param clientId clientId + * @param topicFilter topicFilter + * @param mqttQoS MqttQoS + */ + void onSubscribed(ChannelContext context, String clientId, String topicFilter, MqttQoS mqttQoS); + + /** + * 取消订阅 + * + * @param context ChannelContext + * @param clientId clientId + * @param topicFilter topicFilter + */ + void onUnsubscribed(ChannelContext context, String clientId, String topicFilter); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/IMqttFunctionMessageListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/IMqttFunctionMessageListener.java new file mode 100644 index 0000000..b011568 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/IMqttFunctionMessageListener.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.func; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.tio.core.ChannelContext; + +/** + * mqtt 函数监听器 + * + * @author L.cm + */ +@FunctionalInterface +public interface IMqttFunctionMessageListener { + + /** + * 监听到消息 + * + * @param context ChannelContext + * @param clientId clientId + * @param topic topic + * @param qoS MqttQoS + * @param message Message + */ + void onMessage(ChannelContext context, String clientId, String topic, MqttQoS qoS, MqttPublishMessage message); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/MqttFunctionManager.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/MqttFunctionManager.java new file mode 100644 index 0000000..842acd4 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/MqttFunctionManager.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.func; + +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.tio.utils.hutool.CollUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * mqtt 服务端的函数管理 + * + * @author L.cm + */ +public class MqttFunctionManager { + /** + * root 节点 + */ + private final Node root = new Node("root", null); + + private static class Node { + /** + * topic 片段 + */ + private final String part; + /** + * 监听器集合 + */ + private final List listeners; + /** + * 子节点 + */ + private final Map children; + + public Node(String part) { + this(part, new CopyOnWriteArrayList<>()); + } + + public Node(String part, List listeners) { + this(part, listeners, new ConcurrentHashMap<>(16)); + } + + public Node(String part, List listeners, Map children) { + this.part = part; + this.listeners = listeners; + this.children = children; + } + + /** + * 获取或者添加节点 + * + * @param nodePart nodePart + * @return Node + */ + protected Node addChildIfAbsent(String nodePart) { + assert children != null; + return CollUtil.computeIfAbsent(this.children, nodePart, Node::new); + } + + protected Node findNodeByPart(String nodePart) { + assert children != null; + return children.get(nodePart); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Node node = (Node) o; + return Objects.equals(part, node.part); + } + + @Override + public int hashCode() { + return part != null ? part.hashCode() : 0; + } + + @Override + public String toString() { + return "Node{" + + "part='" + part + '\'' + + '}'; + } + } + + /** + * 注册监听 + * + * @param topicFilters topicFilter array + * @param listener listener + */ + public void register(String[] topicFilters, IMqttFunctionMessageListener listener) { + for (String topicFilter : topicFilters) { + this.register(topicFilter, listener); + } + } + + /** + * 注册监听 + * + * @param topicFilter topicFilter + * @param listener listener + */ + public void register(String topicFilter, IMqttFunctionMessageListener listener) { + Node prev = root; + String[] topicParts = TopicUtil.getTopicParts(topicFilter); + int partLength = topicParts.length - 1; + for (int i = 0; i < topicParts.length; i++) { + prev = prev.addChildIfAbsent(topicParts[i]); + // 判断是否结尾,添加订阅数据 + boolean isEnd = i == partLength; + if (isEnd) { + // 添加监听器 + prev.listeners.add(listener); + } + } + } + + /** + * 获取监听器 + * + * @param topic topic + * @return 监听器集合 + */ + public List get(String topic) { + List listenerList = new ArrayList<>(); + // 这里都是完整的 topic,利用完整的 topic 获取到匹配的监听器 + String[] topicParts = TopicUtil.getTopicParts(topic); + searchListenerRecursively(root, listenerList, topicParts, 0); + return listenerList; + } + + /** + * 递归查找监听器 + * + * @param node node + * @param listenerList listener list + * @param topicParts topic parts + * @param index index + */ + private static void searchListenerRecursively(Node node, List listenerList, String[] topicParts, int index) { + // 层级已经超过,跳出 + if (index >= topicParts.length) { + return; + } + // # 单独处理 + Node nodeMore = node.findNodeByPart(TopicUtil.TOPIC_WILDCARDS_MORE); + if (nodeMore != null) { + listenerList.addAll(nodeMore.listeners); + } + int topicPartLen = topicParts.length - 1; + // + 处理 + Node nodeOne = node.findNodeByPart(TopicUtil.TOPIC_WILDCARDS_ONE); + if (nodeOne != null) { + // 最后一位为 + + if (index == topicPartLen) { + listenerList.addAll(nodeOne.listeners); + } else { + searchListenerRecursively(nodeOne, listenerList, topicParts, index + 1); + } + } + String topicPart = topicParts[index]; + Node nodePart = node.findNodeByPart(topicPart); + if (nodePart != null) { + // 跳出循环 + if (index == topicPartLen) { + listenerList.addAll(nodePart.listeners); + // 判断是否还有 # + Node nodePartMore = nodePart.findNodeByPart(TopicUtil.TOPIC_WILDCARDS_MORE); + if (nodePartMore != null) { + listenerList.addAll(nodePartMore.listeners); + } + } else { + searchListenerRecursively(nodePart, listenerList, topicParts, index + 1); + } + } + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/MqttFunctionMessageListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/MqttFunctionMessageListener.java new file mode 100644 index 0000000..58bf034 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/MqttFunctionMessageListener.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.func; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.server.event.IMqttMessageListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +import java.util.List; + +/** + * 使用函数监听器,方便代码编写 + * + * @author L.cm + */ +public class MqttFunctionMessageListener implements IMqttMessageListener { + private static final Logger logger = LoggerFactory.getLogger(MqttFunctionMessageListener.class); + private final MqttFunctionManager functionManager; + + public MqttFunctionMessageListener(MqttFunctionManager functionManager) { + this.functionManager = functionManager; + } + + @Override + public void onMessage(ChannelContext context, String clientId, String topic, MqttQoS qoS, MqttPublishMessage message) { + List listenerList = functionManager.get(topic); + for (IMqttFunctionMessageListener listener : listenerList) { + try { + listener.onMessage(context, clientId, topic, qoS, message); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + } + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/MqttHttpApi.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/MqttHttpApi.java new file mode 100644 index 0000000..1129e61 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/MqttHttpApi.java @@ -0,0 +1,473 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.api; + +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.dromara.mica.mqtt.core.server.enums.MessageType; +import org.dromara.mica.mqtt.core.server.http.api.code.ResultCode; +import org.dromara.mica.mqtt.core.server.http.api.form.BaseForm; +import org.dromara.mica.mqtt.core.server.http.api.form.PublishForm; +import org.dromara.mica.mqtt.core.server.http.api.form.SubscribeForm; +import org.dromara.mica.mqtt.core.server.http.api.result.Result; +import org.dromara.mica.mqtt.core.server.http.handler.MqttHttpRoutes; +import org.dromara.mica.mqtt.core.server.model.ClientInfo; +import org.dromara.mica.mqtt.core.server.model.Message; +import org.dromara.mica.mqtt.core.server.model.Subscribe; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Tio; +import org.tio.http.common.HttpConst; +import org.tio.http.common.HttpRequest; +import org.tio.http.common.HttpResponse; +import org.tio.http.common.Method; +import org.tio.http.mcp.server.McpServerSession; +import org.tio.http.sse.SseEmitter; +import org.tio.server.TioServerConfig; +import org.tio.utils.hutool.StrUtil; +import org.tio.utils.json.JsonUtil; +import org.tio.utils.mica.PayloadEncode; +import org.tio.utils.timer.TimerTask; +import org.tio.utils.timer.TimerTaskService; + +import java.util.List; +import java.util.function.Function; + +/** + * mqtt http api + * + * @author L.cm + */ +public class MqttHttpApi { + private static final Logger log = LoggerFactory.getLogger(MqttHttpApi.class); + private final MqttServerCreator serverCreator; + private final TioServerConfig mqttServerConfig; + + public MqttHttpApi(MqttServerCreator serverCreator, TioServerConfig mqttServerConfig) { + this.serverCreator = serverCreator; + this.mqttServerConfig = mqttServerConfig; + } + + /** + * 获取 api 列表 + *

+ * GET /api/v1/endpoints + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse endpoints(HttpRequest request) { + return Result.ok(request, MqttHttpRoutes.getRouts().keySet()); + } + + /** + * 获取统计信息 + *

+ * GET /api/v1/stats + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse stats(HttpRequest request) { + return Result.ok(request, this.mqttServerConfig.getStat()); + } + + /** + * 获取统计信息(sse 流) + *

+ * GET /api/v1/stats/sse + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse statsSse(HttpRequest request) { + // sse 频率,默认 3s + int delay = request.getInt("delay", 3000); + HttpResponse response = new HttpResponse(request); + SseEmitter emitter = SseEmitter.getEmitter(request, response); + // 响应包发送后,再发送 sse 回包 + response.setPacketListener((context, packet, isSentSuccess) -> { + if (isSentSuccess) { + TimerTaskService taskService = mqttServerConfig.getTaskService(); + if (taskService != null) { + taskService.addTask((systemTimer -> new TimerTask(delay) { + @Override + public void run() { + try { + if (context != null && !context.isClosed()) { + // 1. 再次添加 任务 + systemTimer.add(this); + // 2. 执行任务 + emitter.send(JsonUtil.toJsonString(mqttServerConfig.getStat())); + } + } catch (Exception e) { + log.error("Tio client schedule error", e); + } + } + })); + } + } + }); + return response; + } + + /** + * 消息发布 + *

+ * POST /api/v1/mqtt/publish + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse publish(HttpRequest request) { + PublishForm form = readForm(request, (requestBody) -> + JsonUtil.readValue(requestBody, PublishForm.class) + ); + if (form == null) { + return Result.fail(request, ResultCode.E101); + } + // 表单校验 + HttpResponse validResponse = validForm(false, form, request); + if (validResponse != null) { + return validResponse; + } + sendPublish(form); + return Result.ok(); + } + + /** + * 消息批量发布 + *

+ * POST /api/v1/mqtt/publish/batch + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse publishBatch(HttpRequest request) { + List formList = readForm(request, (requestBody) -> + JsonUtil.readList(requestBody, PublishForm.class) + ); + if (formList == null || formList.isEmpty()) { + return Result.fail(request, ResultCode.E101); + } + // 参数校验,保证一个批次同时不成功,所以先校验 + for (PublishForm form : formList) { + // 表单校验 + HttpResponse validResponse = validForm(false, form, request); + if (validResponse != null) { + return validResponse; + } + } + // 批量发送 + for (PublishForm form : formList) { + sendPublish(form); + } + return Result.ok(); + } + + private void sendPublish(PublishForm form) { + String payload = form.getPayload(); + Message message = new Message(); + message.setMessageType(MessageType.HTTP_API); + message.setClientId(form.getClientId()); + message.setTopic(form.getTopic()); + message.setQos(form.getQos()); + message.setRetain(form.isRetain()); + // payload 解码 + if (StrUtil.isNotBlank(payload)) { + message.setPayload(PayloadEncode.decode(payload, form.getEncoding())); + } + serverCreator.getMessageDispatcher().send(message); + } + + /** + * 主题订阅 + *

+ * POST /api/v1/mqtt/subscribe + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse subscribe(HttpRequest request) { + SubscribeForm form = readForm(request, (requestBody) -> + JsonUtil.readValue(requestBody, SubscribeForm.class) + ); + if (form == null) { + return Result.fail(request, ResultCode.E101); + } + // 表单校验 + HttpResponse validResponse = validForm(true, form, request); + if (validResponse != null) { + return validResponse; + } + int qos = form.getQos(); + if (qos < 0 || qos > 2) { + return Result.fail(request, ResultCode.E101); + } + // 接口手动添加的订阅关系,可用来调试,不建议其他场景使用 + sendSubOrUnSubscribe(form); + return Result.ok(); + } + + /** + * 主题批量订阅 + *

+ * POST /api/v1/mqtt/subscribe/batch + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse subscribeBatch(HttpRequest request) { + List formList = readForm(request, (requestBody) -> + JsonUtil.readList(requestBody, SubscribeForm.class) + ); + if (formList == null || formList.isEmpty()) { + return Result.fail(request, ResultCode.E101); + } + // 参数校验,保证一个批次同时不成功,所以先校验 + for (SubscribeForm form : formList) { + // 表单校验 + HttpResponse validResponse = validForm(true, form, request); + if (validResponse != null) { + return validResponse; + } + int qos = form.getQos(); + if (qos < 0 || qos > 2) { + return Result.fail(request, ResultCode.E101); + } + } + // 批量处理 + for (SubscribeForm form : formList) { + // 接口手动添加的订阅关系,可用来调试,不建议其他场景使用 + sendSubOrUnSubscribe(form); + } + return Result.ok(); + } + + /** + * 取消订阅 + *

+ * POST /api/v1/mqtt/unsubscribe + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse unsubscribe(HttpRequest request) { + BaseForm form = readForm(request, (requestBody) -> + JsonUtil.readValue(requestBody, BaseForm.class) + ); + if (form == null) { + return Result.fail(request, ResultCode.E101); + } + // 表单校验 + HttpResponse validResponse = validForm(true, form, request); + if (validResponse != null) { + return validResponse; + } + // 接口手动取消的订阅关系,可用来调试,不建议其他场景使用 + sendSubOrUnSubscribe(form); + return Result.ok(); + } + + /** + * 批量取消订阅 + *

+ * POST /api/v1/mqtt/unsubscribe/batch + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse unsubscribeBatch(HttpRequest request) { + List formList = readForm(request, (requestBody) -> + JsonUtil.readList(requestBody, BaseForm.class) + ); + if (formList == null || formList.isEmpty()) { + return Result.fail(request, ResultCode.E101); + } + // 参数校验,保证一个批次同时不成功,所以先校验 + for (BaseForm form : formList) { + // 表单校验 + HttpResponse validResponse = validForm(true, form, request); + if (validResponse != null) { + return validResponse; + } + } + // 批量处理 + for (BaseForm form : formList) { + // 接口手动添加的订阅关系,可用来调试,不建议其他场景使用 + sendSubOrUnSubscribe(form); + } + return Result.ok(); + } + + /** + * 获取取客户端信息 + * + *

+ * GET /api/v1/clients/info + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse getClientInfo(HttpRequest request) { + String clientId = request.getParam("clientId"); + if (StrUtil.isBlank(clientId)) { + return Result.fail(request, ResultCode.E101); + } + ChannelContext context = Tio.getByBsId(this.mqttServerConfig, clientId); + if (context == null) { + return Result.fail(request, ResultCode.E101); + } + ClientInfo clientInfo = ClientInfo.form(serverCreator, context); + return Result.ok(request, clientInfo); + } + + /** + * 分页拉取客户端列表 + * + *

+ * GET /api/v1/clients?_page=1&_limit=10 + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse getClients(HttpRequest request) { + int page = request.getInt("_page", 1); + int limit = request.getInt("_limit", 10000); + return Result.ok(request, MqttServer.getClients(serverCreator, mqttServerConfig, page, limit)); + } + + /** + * 踢除指定客户端。注意踢除客户端操作会将连接与会话一并终结。 + *

+ * POST /api/v1/clients/delete + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse deleteClients(HttpRequest request) { + String clientId = request.getParam("clientId"); + if (StrUtil.isBlank(clientId)) { + return Result.fail(request, ResultCode.E101); + } + Message message = new Message(); + message.setClientId(clientId); + message.setMessageType(MessageType.DISCONNECT); + serverCreator.getMessageDispatcher().send(message); + return Result.ok(); + } + + /** + * 获取客户端订阅情况 + *

+ * GET /api/v1/client/subscriptions + * + * @param request HttpRequest + * @return HttpResponse + */ + public HttpResponse getClientSubscriptions(HttpRequest request) { + String clientId = request.getParam("clientId"); + if (StrUtil.isBlank(clientId)) { + return Result.fail(request, ResultCode.E101); + } + List subscribeList = serverCreator.getSessionManager().getSubscriptions(clientId); + return Result.ok(new HttpResponse(request), subscribeList); + } + + private void sendSubOrUnSubscribe(BaseForm form) { + Message message = new Message(); + message.setFromClientId(form.getClientId()); + message.setTopic(form.getTopic()); + if (form instanceof SubscribeForm) { + message.setQos(((SubscribeForm) form).getQos()); + message.setMessageType(MessageType.SUBSCRIBE); + } else { + message.setMessageType(MessageType.UNSUBSCRIBE); + } + serverCreator.getMessageDispatcher().send(message); + } + + /** + * 读取表单 + * + * @param request HttpRequest + * @param function Function + * @param 泛型 + * @return 表单 + */ + private static T readForm(HttpRequest request, Function function) { + byte[] requestBody = request.getBody(); + if (requestBody == null) { + return null; + } + return function.apply(new String(requestBody, HttpConst.CHARSET)); + } + + /** + * 校验表单 + * + * @param isTopicFilter isTopicFilter + * @param form BaseForm + * @param request HttpRequest + * @return 表单 + */ + private static HttpResponse validForm(boolean isTopicFilter, BaseForm form, HttpRequest request) { + // 必须的参数 + String clientId = form.getClientId(); + if (StrUtil.isBlank(clientId)) { + return Result.fail(request, ResultCode.E101); + } + String topic = form.getTopic(); + if (StrUtil.isBlank(topic)) { + return Result.fail(request, ResultCode.E101); + } + try { + if (isTopicFilter) { + TopicUtil.validateTopicFilter(topic); + } else { + TopicUtil.validateTopicName(topic); + } + } catch (IllegalArgumentException exception) { + return Result.fail(request, ResultCode.E102); + } + return null; + } + + /** + * 注册路由 + */ + public void register() { + // @formatter:off + MqttHttpRoutes.register(Method.GET, "/api/v1/endpoints", this::endpoints); + MqttHttpRoutes.register(Method.GET, "/api/v1/stats", this::stats); + MqttHttpRoutes.register(Method.GET, "/api/v1/stats/sse", this::statsSse); + MqttHttpRoutes.register(Method.POST, "/api/v1/mqtt/publish", this::publish); + MqttHttpRoutes.register(Method.POST, "/api/v1/mqtt/publish/batch", this::publishBatch); + MqttHttpRoutes.register(Method.POST, "/api/v1/mqtt/subscribe", this::subscribe); + MqttHttpRoutes.register(Method.POST, "/api/v1/mqtt/subscribe/batch", this::subscribeBatch); + MqttHttpRoutes.register(Method.POST, "/api/v1/mqtt/unsubscribe", this::unsubscribe); + MqttHttpRoutes.register(Method.POST, "/api/v1/mqtt/unsubscribe/batch", this::unsubscribeBatch); + MqttHttpRoutes.register(Method.GET, "/api/v1/clients/info", this::getClientInfo); + MqttHttpRoutes.register(Method.GET, "/api/v1/clients", this::getClients); + MqttHttpRoutes.register(Method.POST, "/api/v1/clients/delete", this::deleteClients); + MqttHttpRoutes.register(Method.GET, "/api/v1/client/subscriptions", this::getClientSubscriptions); + // @formatter:on + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/auth/BasicAuthFilter.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/auth/BasicAuthFilter.java new file mode 100644 index 0000000..0931967 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/auth/BasicAuthFilter.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.api.auth; + +import org.dromara.mica.mqtt.core.server.http.api.code.ResultCode; +import org.dromara.mica.mqtt.core.server.http.api.result.Result; +import org.dromara.mica.mqtt.core.server.http.handler.HttpFilter; +import org.tio.http.common.HeaderName; +import org.tio.http.common.HeaderValue; +import org.tio.http.common.HttpRequest; +import org.tio.http.common.HttpResponse; +import org.tio.utils.hutool.StrUtil; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; + +/** + * Basic 认证 + * + * @author L.cm + */ +public class BasicAuthFilter implements HttpFilter { + public static final HeaderName WWW_AUTHENTICATE = HeaderName.from("WWW-Authenticate"); + public static final HeaderValue BASIC_REALM = HeaderValue.from("Basic realm=\"Mica mqtt realm\""); + public static final String BASIC_AUTH_HEADER_NAME = "authorization"; + public static final String AUTHORIZATION_PREFIX = "Basic "; + private final String token; + + public BasicAuthFilter(String username, String password) { + this.token = getBasicToken(username, password); + } + + @Override + public boolean filter(HttpRequest request) throws Exception { + String authorization = request.getHeader(BASIC_AUTH_HEADER_NAME); + if (StrUtil.isBlank(authorization)) { + return false; + } + int length = AUTHORIZATION_PREFIX.length(); + if (length >= authorization.length()) { + return false; + } + return token.equals(authorization.substring(length)); + } + + @Override + public HttpResponse response(HttpRequest request) { + HttpResponse response = new HttpResponse(request); + response.addHeader(WWW_AUTHENTICATE, BASIC_REALM); + return Result.fail(response, ResultCode.E103); + } + + private static String getBasicToken(String username, String password) { + Objects.requireNonNull(username, "Basic auth username is null"); + Objects.requireNonNull(password, "Basic auth password is null"); + byte[] tokenBytes = (username + ':' + password).getBytes(StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(tokenBytes); + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/code/ResultCode.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/code/ResultCode.java new file mode 100644 index 0000000..7521d43 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/code/ResultCode.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.api.code; + +import org.tio.http.common.HttpResponseStatus; + +/** + * 响应 code 码 + * + * @author L.cm + */ +public enum ResultCode { + /** + * 成功 + */ + SUCCESS(HttpResponseStatus.C200, 1), + /** + * 关键请求参数缺失 + */ + E101(HttpResponseStatus.C400, 101), + /** + * 请求参数错误 + */ + E102(HttpResponseStatus.C400, 102), + /** + * 用户名或密码错误 + */ + E103(HttpResponseStatus.C401, 103), + /** + * 请求方法错误 + */ + E104(HttpResponseStatus.C405, 104), + /** + * 未知错误 + */ + E105(HttpResponseStatus.C500, 105), + /** + * 请求方法错误 + */ + E404(HttpResponseStatus.C404, 404), + ; + + private final HttpResponseStatus statusCode; + private final int resultCode; + + ResultCode(HttpResponseStatus statusCode, int resultCode) { + this.statusCode = statusCode; + this.resultCode = resultCode; + } + + public HttpResponseStatus getStatusCode() { + return statusCode; + } + + public int getResultCode() { + return resultCode; + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/BaseForm.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/BaseForm.java new file mode 100644 index 0000000..f575e95 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/BaseForm.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.api.form; + +import java.io.Serializable; + +/** + * 基础模型 + * + * @author L.cm + */ +public class BaseForm implements Serializable { + + /** + * 主题 Required + */ + private String topic; + /** + * 客户端标识符 Required + */ + private String clientId; + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/PublishForm.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/PublishForm.java new file mode 100644 index 0000000..51ea121 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/PublishForm.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.api.form; + +/** + * 发布的模型 + * + * @author L.cm + */ +public class PublishForm extends BaseForm { + + /** + * 消息正文 + */ + private String payload; + /** + * 消息正文使用的编码方式,目前仅支持 plain 与 base64 两种 + */ + private String encoding; + /** + * QoS 等级 0 + */ + private int qos = 0; + /** + * 是否为保留消息 + */ + private boolean retain = false; + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public int getQos() { + return qos; + } + + public void setQos(int qos) { + this.qos = qos; + } + + public boolean isRetain() { + return retain; + } + + public void setRetain(boolean retain) { + this.retain = retain; + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/SubscribeForm.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/SubscribeForm.java new file mode 100644 index 0000000..ac87081 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/SubscribeForm.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.api.form; + +/** + * 订阅表单 + * + * @author L.cm + */ +public class SubscribeForm extends BaseForm { + + /** + * QoS 等级 0 + */ + private int qos = 0; + + public int getQos() { + return qos; + } + + public void setQos(int qos) { + this.qos = qos; + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/result/Result.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/result/Result.java new file mode 100644 index 0000000..bae10a8 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/result/Result.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.api.result; + +import org.dromara.mica.mqtt.core.server.http.api.code.ResultCode; +import org.tio.http.common.*; +import org.tio.utils.json.JsonUtil; + +import java.util.HashMap; +import java.util.Map; + +/** + * api Result + * + * @author L.cm + */ +public final class Result { + + /** + * 响应成功 + * + * @return HttpResponse + */ + public static HttpResponse ok() { + return ok(new HttpResponse()); + } + + /** + * 响应成功 + * + * @param response HttpResponse + * @return HttpResponse + */ + public static HttpResponse ok(HttpResponse response) { + return result(response, ResultCode.SUCCESS); + } + + /** + * 响应成功 + * + * @param data Object + * @return HttpResponse + */ + public static HttpResponse ok(Object data) { + return ok(new HttpResponse(), data); + } + + /** + * 响应成功 + * + * @param request HttpRequest + * @param data Object + * @return HttpResponse + */ + public static HttpResponse ok(HttpRequest request, Object data) { + return ok(new HttpResponse(request), data); + } + + /** + * 响应成功 + * + * @param response HttpResponse + * @param data Object + * @return HttpResponse + */ + public static HttpResponse ok(HttpResponse response, Object data) { + ResultCode resultCode = ResultCode.SUCCESS; + Map json = new HashMap<>(4); + json.put("code", resultCode.getResultCode()); + json.put("data", data); + return result(response, resultCode, json); + } + + /** + * 响应失败 + * + * @param resultCode ResultCode + * @return HttpResponse + */ + public static HttpResponse fail(ResultCode resultCode) { + return fail(new HttpResponse(), resultCode); + } + + /** + * 响应失败 + * + * @param request HttpRequest + * @param resultCode ResultCode + * @return HttpResponse + */ + public static HttpResponse fail(HttpRequest request, ResultCode resultCode) { + return fail(new HttpResponse(request), resultCode); + } + + /** + * 响应失败 + * + * @param response HttpResponse + * @param resultCode ResultCode + * @return HttpResponse + */ + public static HttpResponse fail(HttpResponse response, ResultCode resultCode) { + return result(response, resultCode); + } + + private static HttpResponse result(HttpResponse response, ResultCode resultCode) { + Map json = new HashMap<>(2); + json.put("code", resultCode.getResultCode()); + return result(response, resultCode, json); + } + + private static HttpResponse result(HttpResponse response, ResultCode resultCode, Object value) { + response.addHeader(HeaderName.Content_Type, HeaderValue.Content_Type.APPLICATION_JSON); + response.setStatus(resultCode.getStatusCode()); + response.setBody(JsonUtil.toJsonString(value).getBytes(HttpConst.CHARSET)); + response.setCharset(HttpConst.CHARSET); + return response; + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/HttpFilter.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/HttpFilter.java new file mode 100644 index 0000000..61c4482 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/HttpFilter.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.handler; + +import org.tio.http.common.HttpRequest; +import org.tio.http.common.HttpResponse; + +/** + * http 过滤器 + * + * @author L.cm + */ +public interface HttpFilter { + + /** + * 处理请求 + * + * @param request HttpRequest + * @return 可以为null + * @throws Exception Exception + */ + boolean filter(HttpRequest request) throws Exception; + + /** + * 响应 + * + * @param request HttpRequest + * @return HttpResponse + */ + HttpResponse response(HttpRequest request); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/MqttHttpRequestHandler.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/MqttHttpRequestHandler.java new file mode 100644 index 0000000..dd8cbdb --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/MqttHttpRequestHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.handler; + +import org.dromara.mica.mqtt.core.server.http.api.code.ResultCode; +import org.dromara.mica.mqtt.core.server.http.api.result.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.http.common.HttpRequest; +import org.tio.http.common.HttpResponse; +import org.tio.http.common.RequestLine; +import org.tio.http.common.handler.HttpRequestFunction; +import org.tio.http.common.handler.HttpRequestHandler; + +import java.util.List; + +/** + * mqtt http 消息处理 + * + * @author L.cm + */ +public class MqttHttpRequestHandler implements HttpRequestHandler { + private static final Logger logger = LoggerFactory.getLogger(MqttHttpRequestHandler.class); + + @Override + public HttpResponse handler(HttpRequest request) { + RequestLine requestLine = request.getRequestLine(); + // 1. 处理过滤器 + List httpFilters = MqttHttpRoutes.getFilters(); + try { + for (HttpFilter filter : httpFilters) { + if (!filter.filter(request)) { + return filter.response(request); + } + } + } catch (Exception e) { + return resp500(request, requestLine, e); + } + // 2. 路由处理 + HttpRequestFunction handler = MqttHttpRoutes.getHandler(requestLine); + if (handler == null) { + return resp404(request, requestLine); + } + if (logger.isDebugEnabled()) { + logger.debug("mqtt http api {} path:{}", requestLine.method, requestLine.getPathAndQuery()); + } + try { + return handler.apply(request); + } catch (Exception e) { + return resp500(request, requestLine, e); + } + } + + @Override + public HttpResponse resp404(HttpRequest request, RequestLine requestLine) { + if (logger.isWarnEnabled()) { + logger.warn("mqtt http {} path:{} 404", requestLine.getMethod().name(), requestLine.getPathAndQuery()); + } + return Result.fail(request, ResultCode.E404); + } + + @Override + public HttpResponse resp500(HttpRequest request, RequestLine requestLine, Throwable throwable) { + if (logger.isErrorEnabled()) { + logger.error("mqtt http {} path:{} error", requestLine.getMethod().name(), requestLine.getPathAndQuery(), throwable); + } + return Result.fail(request, ResultCode.E105); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/MqttHttpRoutes.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/MqttHttpRoutes.java new file mode 100644 index 0000000..a7c24c1 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/MqttHttpRoutes.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.handler; + +import org.tio.http.common.Method; +import org.tio.http.common.RequestLine; +import org.tio.http.common.handler.HttpRequestFunction; + +import java.util.*; + +/** + * mqtt http api 路由 + * + * @author L.cm + */ +public final class MqttHttpRoutes { + private static final LinkedList FILTERS = new LinkedList<>(); + private static final Map ROUTS = new HashMap<>(); + + /** + * 注册 filter 到 first + * + * @param filter HttpFilter + */ + public static void addFirstFilter(HttpFilter filter) { + FILTERS.addFirst(filter); + } + + /** + * 注册 filter + * + * @param filter HttpFilter + */ + public static void addFilter(HttpFilter filter) { + FILTERS.add(filter); + } + + /** + * 注册 filter + * + * @param index index + * @param filter HttpFilter + */ + public static void addFilter(int index, HttpFilter filter) { + FILTERS.add(index, filter); + } + + /** + * 读取所以的过滤器 + * + * @return 过滤器集合 + */ + public static List getFilters() { + return Collections.unmodifiableList(FILTERS); + } + + /** + * 注册路由 + * + * @param method 请求方法 + * @param path 路径 + * @param function HttpHandler + */ + public static void register(Method method, String path, HttpRequestFunction function) { + ROUTS.put(new RouteInfo(path, method), function); + } + + /** + * 读取路由 + * + * @param requestLine RequestLine + * @return HttpHandler + */ + public static HttpRequestFunction getHandler(RequestLine requestLine) { + String path = requestLine.getPath(); + Method method = requestLine.getMethod(); + return ROUTS.get(new RouteInfo(path, method)); + } + + /** + * 读取所有路由 + * + * @return 路由信息 + */ + public static Map getRouts() { + return Collections.unmodifiableMap(ROUTS); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/RouteInfo.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/RouteInfo.java new file mode 100644 index 0000000..52f58a7 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/RouteInfo.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.handler; + +import org.tio.http.common.Method; + +import java.util.Objects; + +/** + * Handler info + * + * @author L.cm + */ +public class RouteInfo { + private final String path; + private final Method method; + + public RouteInfo(String path, Method method) { + this.path = path; + this.method = method; + } + + public String getPath() { + return path; + } + + public Method getMethod() { + return method; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RouteInfo that = (RouteInfo) o; + return Objects.equals(path, that.path) && method == that.method; + } + + @Override + public int hashCode() { + return Objects.hash(path, method); + } + + @Override + public String toString() { + return "HandlerInfo{" + + "path='" + path + '\'' + + ", method=" + method + + '}'; + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/mcp/MqttMcp.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/mcp/MqttMcp.java new file mode 100644 index 0000000..fbe970c --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/mcp/MqttMcp.java @@ -0,0 +1,291 @@ +package org.dromara.mica.mqtt.core.server.http.mcp; + + +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.dromara.mica.mqtt.core.server.enums.MessageType; +import org.dromara.mica.mqtt.core.server.http.api.form.PublishForm; +import org.dromara.mica.mqtt.core.server.http.handler.MqttHttpRoutes; +import org.dromara.mica.mqtt.core.server.model.Message; +import org.tio.core.stat.vo.StatVo; +import org.tio.http.common.Method; +import org.tio.http.mcp.schema.*; +import org.tio.http.mcp.server.McpServer; +import org.tio.http.mcp.server.McpServerSession; +import org.tio.server.TioServerConfig; +import org.tio.utils.hutool.StrUtil; +import org.tio.utils.json.JsonUtil; +import org.tio.utils.mica.PayloadEncode; +import org.tio.utils.timer.TimerTask; +import org.tio.utils.timer.TimerTaskService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * mqtt mcp 接口 + * + * @author L.cm + */ +public class MqttMcp { + private final MqttServerCreator serverCreator; + private final TioServerConfig mqttServerConfig; + private final McpServer mcpServer; + + public MqttMcp(MqttServerCreator serverCreator, + TioServerConfig mqttServerConfig) { + this(serverCreator, mqttServerConfig, new McpServer()); + } + + public MqttMcp(MqttServerCreator serverCreator, + TioServerConfig mqttServerConfig, + McpServer mcpServer) { + this.serverCreator = serverCreator; + this.mqttServerConfig = mqttServerConfig; + this.mcpServer = mcpServer; + } + + public McpTool getMqttStatsMcpTool() { + String jsonSchema = "{\n" + + " \"type\": \"object\",\n" + + " \"description\": \"统计信息汇总VO,包含连接、消息和节点的统计信息\",\n" + + " \"properties\": {\n" + + " \"connections\": {\n" + + " \"type\": \"object\",\n" + + " \"description\": \"连接相关统计信息\",\n" + + " \"properties\": {\n" + + " \"accepted\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"共接受过的连接数\"\n" + + " },\n" + + " \"size\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"当前连接数\"\n" + + " },\n" + + " \"closed\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"关闭过的连接数\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"messages\": {\n" + + " \"type\": \"object\",\n" + + " \"description\": \"消息相关统计信息\",\n" + + " \"properties\": {\n" + + " \"handledPackets\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"处理的包数量\"\n" + + " },\n" + + " \"handledBytes\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"处理的消息字节数\"\n" + + " },\n" + + " \"receivedPackets\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"接收的包数量\"\n" + + " },\n" + + " \"receivedBytes\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"接收的字节数\"\n" + + " },\n" + + " \"sendPackets\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"发送的包数量\"\n" + + " },\n" + + " \"sendBytes\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"发送的字节数\"\n" + + " },\n" + + " \"bytesPerTcpReceive\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"平均每次TCP包接收的字节数\"\n" + + " },\n" + + " \"packetsPerTcpReceive\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"平均每次TCP包接收的业务包数量\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"nodes\": {\n" + + " \"type\": \"object\",\n" + + " \"description\": \"节点相关统计信息\",\n" + + " \"properties\": {\n" + + " \"clientNodes\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"客户端节点数量\"\n" + + " },\n" + + " \"connections\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"连接数\"\n" + + " },\n" + + " \"users\": {\n" + + " \"type\": \"number\",\n" + + " \"description\": \"用户数\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + + McpTool mcpTool = new McpTool(); + mcpTool.setName("getMqttStatus"); + mcpTool.setDescription("获取 mqtt 状态"); + mcpTool.setReturnDirect(true); + + McpJsonSchema jsonSchemaIn = new McpJsonSchema(); + jsonSchemaIn.setType("object"); + jsonSchemaIn.setProperties(new HashMap<>()); + jsonSchemaIn.setRequired(new ArrayList<>()); + mcpTool.setInputSchema(jsonSchemaIn); + + McpJsonSchema jsonSchemaOut = JsonUtil.readValue(jsonSchema, McpJsonSchema.class); + mcpTool.setOutputSchema(jsonSchemaOut); + return mcpTool; + } + + /** + * 获取 mqtt 状态 + * @param session McpServerSession + * @param params params + * @return McpCallToolResult + */ + public McpCallToolResult getMqttStats(McpServerSession session, Map params) { + StatVo statVo = this.mqttServerConfig.getStat(); + McpCallToolResult toolResult = new McpCallToolResult(); + McpTextContent content = new McpTextContent(JsonUtil.toJsonString(statVo)); + toolResult.setContent(Collections.singletonList(content)); + toolResult.setStructuredContent(statVo); + return toolResult; + } + + public McpTool getMqttPublishMcpTool() { + String jsonSchemaIn = "{\n" + + " \"type\": \"object\",\n" + + " \"description\": \"MQTT 消息发布\",\n" + + " \"properties\": {\n" + + " \"topic\": {\n" + + " \"type\": \"string\",\n" + + " \"description\": \"消息主题\"\n" + + " },\n" + + " \"payload\": {\n" + + " \"type\": \"string\",\n" + + " \"description\": \"消息正文\"\n" + + " },\n" + + " \"encoding\": {\n" + + " \"type\": \"string\",\n" + + " \"description\": \"消息正文编码方式(仅支持 plain 或 base64)\"\n" + + " },\n" + + " \"qos\": {\n" + + " \"type\": \"integer\",\n" + + " \"default\": 0,\n" + + " \"description\": \"QoS 等级(0-2)\"\n" + + " },\n" + + " \"retain\": {\n" + + " \"type\": \"boolean\",\n" + + " \"default\": false,\n" + + " \"description\": \"是否为保留消息\"\n" + + " }\n" + + " },\n" + + " \"required\": [\"topic\", \"payload\"]\n" + + "}"; + + McpTool mcpTool = new McpTool(); + mcpTool.setName("mqttPublish"); + mcpTool.setDescription("mqtt 消息发布"); + mcpTool.setReturnDirect(true); + + // 输入参数 + mcpTool.setInputSchema(JsonUtil.readValue(jsonSchemaIn, McpJsonSchema.class)); + // 输出参数 + McpJsonSchema jsonSchemaOut = new McpJsonSchema(); + jsonSchemaOut.setType("object"); + Map properties = new HashMap<>(); + + Map result = new HashMap<>(); + result.put("type", "boolean"); + result.put("description", "mqtt 发布结果"); + + properties.put("result", result); + jsonSchemaOut.setProperties(properties); + jsonSchemaOut.setRequired(Collections.singletonList("result")); + mcpTool.setOutputSchema(jsonSchemaOut); + return mcpTool; + } + + /** + * 发布 mqtt 消息 + * @param session McpServerSession + * @param params params + * @return McpCallToolResult + */ + public McpCallToolResult mqttPublish(McpServerSession session, Map params) { + PublishForm publishForm = JsonUtil.convertValue(params, PublishForm.class); + boolean result = sendPublish(publishForm); + // 响应结果 + Map json = new HashMap<>(2); + json.put("result", result); + // mcp 结果 + McpCallToolResult toolResult = new McpCallToolResult(); + McpTextContent content = new McpTextContent(JsonUtil.toJsonString(json)); + toolResult.setContent(Collections.singletonList(content)); + toolResult.setStructuredContent(json); + return toolResult; + } + + private boolean sendPublish(PublishForm form) { + String payload = form.getPayload(); + Message message = new Message(); + message.setMessageType(MessageType.HTTP_API); + message.setClientId(form.getClientId()); + message.setTopic(form.getTopic()); + message.setQos(form.getQos()); + message.setRetain(form.isRetain()); + // payload 解码 + if (StrUtil.isNotBlank(payload)) { + message.setPayload(PayloadEncode.decode(payload, form.getEncoding())); + } + return serverCreator.getMessageDispatcher().send(message); + } + + /** + * 注册 mcp + */ + public void register() { + // 注册路由 + MqttHttpRoutes.register(Method.GET, mcpServer.getSseEndpoint(), mcpServer::sseEndpoint); + MqttHttpRoutes.register(Method.POST, mcpServer.getMessageEndpoint(), mcpServer::sseMessageEndpoint); + // mcp 信息 + McpServerCapabilities serverCapabilities = new McpServerCapabilities(); + McpLoggingCapabilities logging = new McpLoggingCapabilities(); + serverCapabilities.setLogging(logging); + McpPromptCapabilities prompts = new McpPromptCapabilities(); + prompts.setListChanged(false); + serverCapabilities.setPrompts(prompts); + McpResourceCapabilities resources = new McpResourceCapabilities(); + resources.setListChanged(false); + resources.setSubscribe(false); + serverCapabilities.setResources(resources); + // 只暴露 tools + McpToolCapabilities tools = new McpToolCapabilities(); + tools.setListChanged(true); + serverCapabilities.setTools(tools); + mcpServer.capabilities(serverCapabilities); + // 注册 tools + mcpServer.tool(getMqttStatsMcpTool(), this::getMqttStats); + mcpServer.tool(getMqttPublishMcpTool(), this::mqttPublish); + // 开启心跳 + TimerTaskService taskService = mqttServerConfig.getTaskService(); + taskService.addTask((systemTimer -> new TimerTask(TimeUnit.SECONDS.toMillis(15)) { + @Override + public void run() { + // 1. 再次添加 任务 + systemTimer.add(this); + // 2. 发送心跳 + mcpServer.sendHeartbeat(); + } + })); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/websocket/MqttWsMsgHandler.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/websocket/MqttWsMsgHandler.java new file mode 100644 index 0000000..9c68fbf --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/websocket/MqttWsMsgHandler.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.websocket; + +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.core.server.MqttMessageInterceptors; +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.TioConfig; +import org.tio.core.intf.Packet; +import org.tio.core.intf.TioHandler; +import org.tio.http.common.HttpRequest; +import org.tio.http.common.HttpResponse; +import org.tio.utils.buffer.ByteBufferUtil; +import org.tio.utils.hutool.FastByteBuffer; +import org.tio.websocket.common.WsRequest; +import org.tio.websocket.common.WsResponse; +import org.tio.websocket.server.handler.IWsMsgHandler; + +import java.nio.ByteBuffer; + +/** + * mqtt websocket 消息处理 + * + * @author L.cm + */ +public class MqttWsMsgHandler implements IWsMsgHandler { + private static final Logger logger = LoggerFactory.getLogger(MqttWsMsgHandler.class); + + /** + * mqtt websocket message body key + */ + private static final String MQTT_WS_MSG_BODY_KEY = "MQTT_WS_MSG_BODY_KEY"; + /** + * websocket 握手端点 + */ + private final String[] supportedSubProtocols; + private final TioHandler mqttServerAioHandler; + private final MqttMessageInterceptors messageInterceptors; + + public MqttWsMsgHandler(MqttServerCreator serverCreator, TioHandler handler) { + this(serverCreator, new String[]{"mqtt", "mqttv3.1", "mqttv3.1.1"}, handler); + } + + public MqttWsMsgHandler(MqttServerCreator serverCreator, + String[] supportedSubProtocols, + TioHandler handler) { + this.supportedSubProtocols = supportedSubProtocols; + this.mqttServerAioHandler = handler; + this.messageInterceptors = serverCreator.getMessageInterceptors(); + } + + @Override + public String[] getSupportedSubProtocols() { + return this.supportedSubProtocols; + } + + /** + * 握手后处理 + * + * @param request HttpRequest + * @param response HttpResponse + * @param context ChannelContext + */ + @Override + public void onAfterHandshaked(HttpRequest request, HttpResponse response, ChannelContext context) { + // 在连接中添加 WriteBuffer 用来处理半包消息 + context.computeIfAbsent(MQTT_WS_MSG_BODY_KEY, key -> new FastByteBuffer()); + } + + /** + * 字节消息(binaryType = arraybuffer)过来后会走这个方法 + */ + @Override + public Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext context) throws Exception { + FastByteBuffer wsBody = context.get(MQTT_WS_MSG_BODY_KEY); + ByteBuffer buffer = getMqttBody(wsBody, bytes); + if (buffer == null) { + return null; + } + // 可能会一次有多个包,所以需要进行拆包 + while (buffer.hasRemaining()) { + // 解析 mqtt 消息 + int readableLength = buffer.remaining(); + Packet packet = mqttServerAioHandler.decode(buffer, 0, 0, readableLength, context); + if (packet == null) { + // 如果拆包之后还有剩余,写回到 WriteBuffer + int remaining = buffer.remaining(); + if (remaining > 0) { + byte[] data = new byte[remaining]; + buffer.get(data); + wsBody.writeBytes(data); + } + return null; + } + MqttMessage mqttMessage = (MqttMessage) packet; + // 消息解析后 + try { + messageInterceptors.onAfterDecoded(context, mqttMessage, readableLength); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + // 消息处理 + mqttServerAioHandler.handler(packet, context); + // 消息处理后 + try { + messageInterceptors.onAfterHandled(context, mqttMessage, readableLength); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + } + return null; + } + + @Override + public WsResponse encodeSubProtocol(Packet packet, TioConfig tioConfig, ChannelContext context) { + if (packet instanceof MqttMessage) { + ByteBuffer buffer = mqttServerAioHandler.encode(packet, null, context); + return WsResponse.fromBytes(buffer.array()); + } + return null; + } + + /** + * 当客户端发 close flag 时,会走这个方法 + */ + @Override + public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext context) { + context.remove(MQTT_WS_MSG_BODY_KEY); + return null; + } + + /** + * 字符消息(binaryType = blob)过来后会走这个方法 + */ + @Override + public Object onText(WsRequest wsRequest, String text, ChannelContext context) { + return null; + } + + /** + * 读取 mqtt 消息体处理半包的情况 + * + * @param bytes 消息类容 + * @return ByteBuffer + */ + private static synchronized ByteBuffer getMqttBody(FastByteBuffer wsBody, byte[] bytes) { + wsBody.writeBytes(bytes); + int length = wsBody.size(); + if (length < 2) { + return null; + } + ByteBuffer buffer = wsBody.toBuffer(); + Integer mqttLength = getMqttLength(buffer); + if (mqttLength == null || length < (mqttLength + 2)) { + return null; + } + // 数据已经读取完毕,此处需要重构 + wsBody.reset(); + // 重置 buffer + buffer.rewind(); + return buffer; + } + + /** + * 解析 mqtt 消息长度 + * + * @param buffer the buffer to decode from + * @return mqtt 消息长度 + */ + private static Integer getMqttLength(ByteBuffer buffer) { + ByteBufferUtil.skipBytes(buffer, 1); + int remainingLength = 0; + int multiplier = 1; + short digit; + int loops = 0; + do { + if (!buffer.hasRemaining()) { + return null; + } + digit = ByteBufferUtil.readUnsignedByte(buffer); + remainingLength += (digit & 127) * multiplier; + multiplier *= 128; + loops++; + } while ((digit & 128) != 0 && loops < 4); + return remainingLength; + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/websocket/MqttWsServerListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/websocket/MqttWsServerListener.java new file mode 100644 index 0000000..378c465 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/websocket/MqttWsServerListener.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.http.websocket; + +import org.tio.core.ChannelContext; +import org.tio.core.intf.Packet; +import org.tio.server.intf.TioServerListener; +import org.tio.websocket.server.WsTioServerListener; + +/** + * mqtt websocket 监听器 + * + * @author L.cm + */ +public class MqttWsServerListener extends WsTioServerListener { + private final TioServerListener serverListener; + + public MqttWsServerListener(TioServerListener serverListener) { + this.serverListener = serverListener; + } + + @Override + public void onAfterConnected(ChannelContext context, boolean isConnected, boolean isReconnect) throws Exception { + super.onAfterConnected(context, isConnected, isReconnect); + serverListener.onAfterConnected(context, isConnected, isReconnect); + } + + @Override + public boolean onHeartbeatTimeout(ChannelContext context, long interval, int heartbeatTimeoutCount) { + return serverListener.onHeartbeatTimeout(context, interval, heartbeatTimeoutCount); + } + + @Override + public void onAfterDecoded(ChannelContext context, Packet packet, int packetSize) throws Exception { + serverListener.onAfterDecoded(context, packet, packetSize); + } + + @Override + public void onAfterReceivedBytes(ChannelContext context, int receivedBytes) throws Exception { + serverListener.onAfterReceivedBytes(context, receivedBytes); + } + + @Override + public void onAfterSent(ChannelContext context, Packet packet, boolean isSentSuccess) throws Exception { + serverListener.onAfterSent(context, packet, isSentSuccess); + } + + @Override + public void onAfterHandled(ChannelContext context, Packet packet, long cost) throws Exception { + serverListener.onAfterHandled(context, packet, cost); + } + + @Override + public void onBeforeClose(ChannelContext context, Throwable throwable, String remark, boolean isRemove) throws Exception { + serverListener.onBeforeClose(context, throwable, remark, isRemove); + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/interceptor/IMqttMessageInterceptor.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/interceptor/IMqttMessageInterceptor.java new file mode 100644 index 0000000..29bc882 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/interceptor/IMqttMessageInterceptor.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.interceptor; + +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.tio.core.ChannelContext; + +/** + * mqtt 消息拦截器 + * + * @author L.cm + */ +public interface IMqttMessageInterceptor { + + /** + * 建链后触发本方法,注:建链不一定成功,需要关注参数isConnected + * + * @param context ChannelContext + * @param isConnected 是否连接成功,true:表示连接成功,false:表示连接失败 + * @param isReconnect 是否是重连, true: 表示这是重新连接,false: 表示这是第一次连接 + * @throws Exception Exception + */ + default void onAfterConnected(ChannelContext context, boolean isConnected, boolean isReconnect) throws Exception { + + } + + /** + * 接收到TCP层传过来的数据后 + * + * @param context ChannelContext + * @param receivedBytes 本次接收了多少字节 + * @throws Exception Exception + */ + default void onAfterReceivedBytes(ChannelContext context, int receivedBytes) throws Exception { + + } + + /** + * 解码成功后触发本方法 + * + * @param context ChannelContext + * @param message MqttMessage + * @param packetSize packetSize + * @throws Exception Exception + */ + default void onAfterDecoded(ChannelContext context, MqttMessage message, int packetSize) throws Exception { + + } + + /** + * 处理一个消息包后 + * + * @param context ChannelContext + * @param message MqttMessage + * @param cost 本次处理消息耗时,单位:毫秒 + * @throws Exception Exception + */ + default void onAfterHandled(ChannelContext context, MqttMessage message, long cost) throws Exception { + + } + + /** + * 处理一个消息包后 + * + * @param context ChannelContext + * @param message MqttMessage + * @param isSentSuccess 是否发送成功 + * @throws Exception Exception + */ + default void onAfterSent(ChannelContext context, MqttMessage message, boolean isSentSuccess) throws Exception { + + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/IMqttProtocolListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/IMqttProtocolListener.java new file mode 100644 index 0000000..cd1c78e --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/IMqttProtocolListener.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.listener; + +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.dromara.mica.mqtt.core.server.protocol.MqttProtocol; +import org.tio.core.Node; +import org.tio.core.ssl.SslConfig; +import org.tio.server.TioServer; +import org.tio.server.TioServerConfig; + +/** + * mqtt 监听器 + * + * @author L.cm + */ +public interface IMqttProtocolListener { + /** + * 获取协议 + * + * @return MqttProtocol + */ + MqttProtocol getProtocol(); + + /** + * 获取 ip 断开 + * + * @return ServerNode + */ + Node getServerNode(); + + /** + * ssl 配置 + * + * @return SslConfig + */ + SslConfig getSslConfig(); + + /** + * 配置服务 + * + * @param serverCreator serverCreator + * @param mqttServerConfig mqttServerConfig + * @return TioServer + */ + TioServer config(MqttServerCreator serverCreator, TioServerConfig mqttServerConfig); + + /** + * 处理默认的 server Node + * + * @param serverNode serverNode + * @param protocol MqttProtocol + * @return server Node + */ + static Node getServerNode(Node serverNode, MqttProtocol protocol) { + return serverNode == null ? new Node(null, protocol.getPort()) : serverNode; + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttHttpApiListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttHttpApiListener.java new file mode 100644 index 0000000..3d9677d --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttHttpApiListener.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.listener; + +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.dromara.mica.mqtt.core.server.http.api.MqttHttpApi; +import org.dromara.mica.mqtt.core.server.http.api.auth.BasicAuthFilter; +import org.dromara.mica.mqtt.core.server.http.handler.HttpFilter; +import org.dromara.mica.mqtt.core.server.http.handler.MqttHttpRequestHandler; +import org.dromara.mica.mqtt.core.server.http.handler.MqttHttpRoutes; +import org.dromara.mica.mqtt.core.server.http.mcp.MqttMcp; +import org.dromara.mica.mqtt.core.server.protocol.MqttProtocol; +import org.tio.core.Node; +import org.tio.core.TcpConst; +import org.tio.core.ssl.ClientAuth; +import org.tio.core.ssl.SslConfig; +import org.tio.core.uuid.SeqTioUuid; +import org.tio.http.common.HttpConfig; +import org.tio.http.mcp.server.McpServer; +import org.tio.http.server.HttpTioServerHandler; +import org.tio.http.server.HttpTioServerListener; +import org.tio.server.TioServer; +import org.tio.server.TioServerConfig; + +import java.io.InputStream; +import java.util.Objects; + +/** + * mqtt http api 监听器 + * + * @author L.cm + */ +public class MqttHttpApiListener implements IMqttProtocolListener { + private static final MqttProtocol PROTOCOL = MqttProtocol.MQTT_HTTP_API; + private final Node serverNode; + private final HttpFilter authFilter; + private final McpServer mcpServer; + private final SslConfig sslConfig; + + MqttHttpApiListener(Node serverNode, + HttpFilter authFilter, + McpServer mcpServer, + SslConfig sslConfig) { + this.serverNode = IMqttProtocolListener.getServerNode(serverNode, PROTOCOL); + this.authFilter = authFilter; + this.mcpServer = mcpServer; + this.sslConfig = sslConfig; + } + + @Override + public MqttProtocol getProtocol() { + return PROTOCOL; + } + + @Override + public Node getServerNode() { + return this.serverNode; + } + + public HttpFilter getAuthFilter() { + return authFilter; + } + + public McpServer getMcpServer() { + return mcpServer; + } + + @Override + public SslConfig getSslConfig() { + return this.sslConfig; + } + + @Override + public TioServer config(MqttServerCreator serverCreator, TioServerConfig mqttServerConfig) { + // 1 http 路由配置 + MqttHttpApi httpApi = new MqttHttpApi(serverCreator, mqttServerConfig); + httpApi.register(); + // 2 认证配置 + if (authFilter != null) { + MqttHttpRoutes.addFilter(authFilter); + } + // 3. 是否开启 mcp + if (mcpServer != null) { + MqttMcp mqttMcp = new MqttMcp(serverCreator, mqttServerConfig, mcpServer); + mqttMcp.register(); + } + // 4. http 服务配置 + TioServerConfig tioServerConfig = new TioServerConfig( + serverCreator.getName() + '/' + this.getProtocol().name(), + new HttpTioServerHandler(new HttpConfig(), new MqttHttpRequestHandler()), + new HttpTioServerListener(), + mqttServerConfig.tioExecutor, mqttServerConfig.groupExecutor + ); + tioServerConfig.setTaskService(mqttServerConfig.getTaskService()); + // 5. 心跳超时时间,默认 30s + tioServerConfig.setHeartbeatTimeout(1000 * 30L); + tioServerConfig.setShortConnection(true); + tioServerConfig.setReadBufferSize(TcpConst.MAX_DATA_LENGTH); + tioServerConfig.setTioUuid(new SeqTioUuid()); + tioServerConfig.setSslConfig(sslConfig); + // http api 不开启 debug 和 stat + tioServerConfig.debug = false; + tioServerConfig.statOn = false; + return new TioServer(serverNode, tioServerConfig); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + IMqttProtocolListener that = (IMqttProtocolListener) o; + return Objects.equals(serverNode, that.getServerNode()); + } + + @Override + public int hashCode() { + return serverNode.hashCode(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Node serverNode; + private HttpFilter authFilter; + private McpServer mcpServer; + private SslConfig sslConfig; + + public Builder serverNode(int port) { + return this.serverNode(new Node(null, port)); + } + + public Builder serverNode(String ip, int port) { + return this.serverNode(new Node(ip, port)); + } + + public Builder serverNode(Node serverNode) { + this.serverNode = serverNode; + return this; + } + + public Builder authFilter(HttpFilter authFilter) { + this.authFilter = authFilter; + return this; + } + + public Builder basicAuth(String username, String password) { + return this.authFilter(new BasicAuthFilter(username, password)); + } + + public Builder mcpServer(McpServer mcpServer) { + this.mcpServer = mcpServer; + return this; + } + + public Builder mcpServer() { + return mcpServer(new McpServer()); + } + + public Builder mcpServer(String sseEndpoint, String messageEndpoint) { + return mcpServer(new McpServer(sseEndpoint, messageEndpoint)); + } + + public Builder sslConfig(SslConfig sslConfig) { + this.sslConfig = sslConfig; + return this; + } + + public Builder useSsl(InputStream keyStoreInputStream, String keyPasswd) { + return sslConfig(SslConfig.forServer(keyStoreInputStream, keyPasswd)); + } + + public Builder useSsl(InputStream keyStoreInputStream, String keyPasswd, ClientAuth clientAuth) { + return sslConfig(SslConfig.forServer(keyStoreInputStream, keyPasswd, clientAuth)); + } + + public Builder useSsl(InputStream keyStoreInputStream, String keyPasswd, InputStream trustStoreInputStream, String trustPassword, ClientAuth clientAuth) { + return sslConfig(SslConfig.forServer(keyStoreInputStream, keyPasswd, trustStoreInputStream, trustPassword, clientAuth)); + } + + public Builder useSsl(String keyStoreFile, String keyPasswd) { + return sslConfig(SslConfig.forServer(keyStoreFile, keyPasswd)); + } + + public Builder useSsl(String keyStoreFile, String keyPasswd, ClientAuth clientAuth) { + return sslConfig(SslConfig.forServer(keyStoreFile, keyPasswd, clientAuth)); + } + + public Builder useSsl(String keyStoreFile, String keyPasswd, String trustStoreFile, String trustPassword, ClientAuth clientAuth) { + return sslConfig(SslConfig.forServer(keyStoreFile, keyPasswd, trustStoreFile, trustPassword, clientAuth)); + } + + public MqttHttpApiListener build() { + return new MqttHttpApiListener(serverNode, authFilter, mcpServer, sslConfig); + } + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttProtocolListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttProtocolListener.java new file mode 100644 index 0000000..7382360 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttProtocolListener.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.listener; + +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.dromara.mica.mqtt.core.server.http.websocket.MqttWsMsgHandler; +import org.dromara.mica.mqtt.core.server.http.websocket.MqttWsServerListener; +import org.dromara.mica.mqtt.core.server.protocol.MqttProtocol; +import org.tio.core.Node; +import org.tio.core.ssl.ClientAuth; +import org.tio.core.ssl.SslConfig; +import org.tio.core.uuid.SnowflakeTioUuid; +import org.tio.http.common.HttpConfig; +import org.tio.server.TioServer; +import org.tio.server.TioServerConfig; +import org.tio.websocket.server.WsTioServerHandler; + +import java.io.InputStream; +import java.util.Objects; + +/** + * mqtt 协议监听器 + * + * @author L.cm + */ +public class MqttProtocolListener implements IMqttProtocolListener { + private final MqttProtocol protocol; + private final Node serverNode; + private final SslConfig sslConfig; + + public MqttProtocolListener(MqttProtocol protocol, Node serverNode) { + this(protocol, serverNode, null); + } + + public MqttProtocolListener(MqttProtocol protocol, Node serverNode, SslConfig sslConfig) { + this.protocol = checkSSl(protocol, sslConfig); + this.serverNode = IMqttProtocolListener.getServerNode(serverNode, protocol); + this.sslConfig = sslConfig; + } + + private static MqttProtocol checkSSl(MqttProtocol mqttProtocol, SslConfig sslConfig) { + if ((MqttProtocol.MQTT_SSL == mqttProtocol || MqttProtocol.MQTT_WSS == mqttProtocol) && sslConfig == null) { + throw new NullPointerException(mqttProtocol + " 缺少必要参数 SslConfig"); + } else { + return Objects.requireNonNull(mqttProtocol, "MqttProtocol is null"); + } + } + + @Override + public MqttProtocol getProtocol() { + return this.protocol; + } + + @Override + public Node getServerNode() { + return this.serverNode; + } + + @Override + public SslConfig getSslConfig() { + return this.sslConfig; + } + + @Override + public TioServer config(MqttServerCreator serverCreator, TioServerConfig mainServerConfig) { + // 1. 服务配置 + TioServerConfig serverConfig; + if (this.protocol == MqttProtocol.MQTT || this.protocol == MqttProtocol.MQTT_SSL) { + serverConfig = getTcpServerConfig(serverCreator, mainServerConfig); + } else { + serverConfig = getWebSocketServerConfig(serverCreator, mainServerConfig); + } + serverConfig.setUseQueueDecode(mainServerConfig.useQueueDecode); + serverConfig.setUseQueueSend(mainServerConfig.useQueueSend); + serverConfig.setTaskService(mainServerConfig.getTaskService()); + // 2. 消息默认的心跳 + serverConfig.setHeartbeatTimeout(0); + int readBufferSize = mainServerConfig.getReadBufferSize(); + if (readBufferSize > 0) { + serverConfig.setReadBufferSize(readBufferSize); + } + // 3. 是否开启监控和 debug + serverConfig.statOn = mainServerConfig.statOn; + serverConfig.debug = mainServerConfig.debug; + // 4. 如果开启了 ssl + serverConfig.setSslConfig(sslConfig); + // 5. 共享配置 + serverConfig.share(mainServerConfig); + return new TioServer(serverNode, serverConfig); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + IMqttProtocolListener that = (IMqttProtocolListener) o; + return Objects.equals(serverNode, that.getServerNode()); + } + + @Override + public int hashCode() { + return Objects.hashCode(serverNode); + } + + /** + * 获取 tcp 服务配置 + * + * @param serverCreator serverCreator + * @param mainServerConfig mainServerConfig + * @return TioServerConfig + */ + private TioServerConfig getTcpServerConfig(MqttServerCreator serverCreator, TioServerConfig mainServerConfig) { + // tcp 服务配置 + TioServerConfig serverConfig = new TioServerConfig(serverCreator.getName() + '/' + this.getProtocol().name(), + mainServerConfig.getTioServerHandler(), mainServerConfig.getTioServerListener()); + // 是否开启代理协议 + serverConfig.enableProxyProtocol(mainServerConfig.isProxyProtocolEnabled()); + return serverConfig; + } + + /** + * 获取 websocket 配置 + * + * @param serverCreator MqttServerCreator + * @param mainServerConfig TioServerConfig + * @return TioServerConfig + */ + private TioServerConfig getWebSocketServerConfig(MqttServerCreator serverCreator, TioServerConfig mainServerConfig) { + // mqtt websocket 处理器 + MqttWsMsgHandler handler = new MqttWsMsgHandler(serverCreator, mainServerConfig.getTioHandler()); + // 配置 + TioServerConfig tioServerConfig = new TioServerConfig( + serverCreator.getName() + '/' + this.getProtocol().name(), + new WsTioServerHandler(new HttpConfig(), handler), + new MqttWsServerListener(mainServerConfig.getTioServerListener()), + mainServerConfig.tioExecutor, + mainServerConfig.groupExecutor + ); + tioServerConfig.setTioUuid(new SnowflakeTioUuid()); + return tioServerConfig; + } + + public static Builder mqttBuilder() { + return new Builder(MqttProtocol.MQTT); + } + + public static SslBuilder mqttSslBuilder() { + return new SslBuilder(MqttProtocol.MQTT_SSL); + } + + public static Builder wsBuilder() { + return new Builder(MqttProtocol.MQTT_WS); + } + + public static SslBuilder wssBuilder() { + return new SslBuilder(MqttProtocol.MQTT_WSS); + } + + public static class Builder { + protected final MqttProtocol protocol; + protected Node serverNode; + + Builder(MqttProtocol protocol) { + this.protocol = protocol; + } + + public Builder serverNode(int port) { + return serverNode(null, port); + } + + public Builder serverNode(String ip, int port) { + return this.serverNode(new Node(ip, port)); + } + + public Builder serverNode(Node serverNode) { + this.serverNode = serverNode; + return this; + } + + public MqttProtocolListener build() { + return new MqttProtocolListener(protocol, serverNode, null); + } + } + + public static class SslBuilder extends Builder { + private SslConfig sslConfig; + + SslBuilder(MqttProtocol protocol) { + super(protocol); + } + + @Override + public SslBuilder serverNode(int port) { + return serverNode(null, port); + } + + @Override + public SslBuilder serverNode(String ip, int port) { + return this.serverNode(new Node(ip, port)); + } + + @Override + public SslBuilder serverNode(Node serverNode) { + this.serverNode = serverNode; + return this; + } + + public SslBuilder sslConfig(SslConfig sslConfig) { + this.sslConfig = sslConfig; + return this; + } + + public SslBuilder useSsl(InputStream keyStoreInputStream, String keyPasswd) { + return sslConfig(SslConfig.forServer(keyStoreInputStream, keyPasswd)); + } + + public SslBuilder useSsl(InputStream keyStoreInputStream, String keyPasswd, ClientAuth clientAuth) { + return sslConfig(SslConfig.forServer(keyStoreInputStream, keyPasswd, clientAuth)); + } + + public SslBuilder useSsl(InputStream keyStoreInputStream, String keyPasswd, InputStream trustStoreInputStream, String trustPassword, ClientAuth clientAuth) { + return sslConfig(SslConfig.forServer(keyStoreInputStream, keyPasswd, trustStoreInputStream, trustPassword, clientAuth)); + } + + public SslBuilder useSsl(String keyStoreFile, String keyPasswd) { + return sslConfig(SslConfig.forServer(keyStoreFile, keyPasswd)); + } + + public SslBuilder useSsl(String keyStoreFile, String keyPasswd, ClientAuth clientAuth) { + return sslConfig(SslConfig.forServer(keyStoreFile, keyPasswd, clientAuth)); + } + + public SslBuilder useSsl(String keyStoreFile, String keyPasswd, String trustStoreFile, String trustPassword, ClientAuth clientAuth) { + return sslConfig(SslConfig.forServer(keyStoreFile, keyPasswd, trustStoreFile, trustPassword, clientAuth)); + } + + @Override + public MqttProtocolListener build() { + return new MqttProtocolListener(protocol, serverNode, sslConfig); + } + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttProtocolListeners.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttProtocolListeners.java new file mode 100644 index 0000000..b32afdd --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttProtocolListeners.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.listener; + +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.server.TioServer; +import org.tio.server.TioServerConfig; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * mqtt 协议监听器 + * + * @author L.cm + */ +public class MqttProtocolListeners { + private static final Logger logger = LoggerFactory.getLogger(MqttProtocolListeners.class); + private final List servers; + + public MqttProtocolListeners(MqttServerCreator serverCreator, + TioServerConfig mqttServerConfig, + List listeners) { + this.servers = getTioServers(serverCreator, mqttServerConfig, listeners); + } + + private static List getTioServers(MqttServerCreator serverCreator, + TioServerConfig mqttServerConfig, + List listeners) { + List servers = new ArrayList<>(); + for (IMqttProtocolListener listener : listeners) { + servers.add(listener.config(serverCreator, mqttServerConfig)); + } + return servers; + } + + /** + * 启动监听器 + */ + public void start() { + for (TioServer server : servers) { + try { + server.start(); + } catch (IOException e) { + String serverConfigName = server.getServerConfig().getName(); + String message = serverConfigName + ' ' + server.getServerNode() + " start fail."; + throw new IllegalStateException(message, e); + } + } + } + + /** + * 停止监听器 + * + * @return 是否停止 + */ + public boolean stop() { + boolean result = true; + for (TioServer server : servers) { + result &= server.stop(); + logger.info("{} stop result:{}", server.getServerConfig().getName(), result); + } + return result; + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/ClientInfo.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/ClientInfo.java new file mode 100644 index 0000000..6822e02 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/ClientInfo.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.model; + +import org.dromara.mica.mqtt.codec.MqttCodecUtil; +import org.dromara.mica.mqtt.codec.MqttVersion; +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.tio.core.ChannelContext; +import org.tio.core.Node; + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +/** + * 客户端信息 + * + * @author L.cm + */ +public class ClientInfo implements Serializable { + + /** + * 客户端 Id + */ + private String clientId; + /** + * 用户名 + */ + private String username; + /** + * keep alive + */ + private long keepAlive; + /** + * 连接成功 + */ + private boolean connected; + /** + * 协议名称 + */ + private String protoName; + /** + * 协议版本 + */ + private int protoVer; + /** + * 协议全名 + */ + private String protoFullName; + /** + * 是否 WebSocket + */ + private boolean webSocket; + /** + * 是否开启 SSL + */ + private boolean ssl; + /** + * ip + */ + private String ipAddress; + /** + * 端口 + */ + private int port; + /** + * 连接成功时间 + */ + private Long connectedAt; + /** + * 创建时间 + */ + private long createdAt; + /** + * 解码队列长度 + */ + private int decodeQueueSize; + /** + * 业务处理队列长度 + */ + private int handlerQueueSize; + /** + * 发送队列长度 + */ + private int sendQueueSize; + + /** + * 构造 ClientInfo + * + * @param serverCreator MqttServerCreator + * @param context ChannelContext + * @return ClientInfo + */ + public static ClientInfo form(MqttServerCreator serverCreator, ChannelContext context) { + return form(serverCreator, context, ClientInfo::new); + } + + /** + * 构造 ClientInfo + * + * @param serverCreator MqttServerCreator + * @param context ChannelContext + * @param extFunc Supplier + * @return ClientInfo + */ + public static ClientInfo form(MqttServerCreator serverCreator, ChannelContext context, Supplier extFunc) { + ClientInfo clientInfo = extFunc.get(); + // 客户端信息 + clientInfo.setClientId(context.getBsId()); + clientInfo.setUsername(context.getUserId()); + clientInfo.setConnected(context.isAccepted()); + // keepAlive 心跳时间 + clientInfo.setKeepAlive(getClientKeepAlive(context, serverCreator)); + // ip、port + Node clientNode = context.getClientNode(); + clientInfo.setIpAddress(clientNode.getIp()); + clientInfo.setPort(clientNode.getPort()); + // mqtt 版本信息 + MqttVersion mqttVersion = MqttCodecUtil.getMqttVersion(context); + clientInfo.setProtoName(mqttVersion.protocolName()); + clientInfo.setProtoVer(mqttVersion.protocolLevel()); + clientInfo.setProtoFullName(mqttVersion.fullName()); + clientInfo.setSsl(context.isSsl()); + clientInfo.setWebSocket(context.containsKey("TIO_W_S_C")); + // 时间信息 + clientInfo.setConnectedAt(context.stat.timeFirstConnected); + clientInfo.setCreatedAt(context.stat.timeCreated); + // 队列信息 + clientInfo.setDecodeQueueSize(context.getDecodeQueueSize()); + clientInfo.setHandlerQueueSize(context.getHandlerQueueSize()); + clientInfo.setSendQueueSize(context.getSendQueueSize()); + return clientInfo; + } + + + /** + * 获取客户端的心跳时长 + * + * @param context ChannelContext + * @param serverCreator MqttServerCreator + * @return 心跳时长 + */ + public static long getClientKeepAlive(ChannelContext context, MqttServerCreator serverCreator) { + // keepAlive + if (context.heartbeatTimeout == null) { + return 60L; + } else { + float keepAliveBackoff = serverCreator.getKeepaliveBackoff(); + long keepAliveTs = (long) (context.heartbeatTimeout / (keepAliveBackoff * 2)); + return TimeUnit.MILLISECONDS.toSeconds(keepAliveTs); + } + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public long getKeepAlive() { + return keepAlive; + } + + public void setKeepAlive(long keepAlive) { + this.keepAlive = keepAlive; + } + + public boolean isConnected() { + return connected; + } + + public void setConnected(boolean connected) { + this.connected = connected; + } + + public String getProtoName() { + return protoName; + } + + public void setProtoName(String protoName) { + this.protoName = protoName; + } + + public int getProtoVer() { + return protoVer; + } + + public void setProtoVer(int protoVer) { + this.protoVer = protoVer; + } + + public String getProtoFullName() { + return protoFullName; + } + + public void setProtoFullName(String protoFullName) { + this.protoFullName = protoFullName; + } + + public boolean isWebSocket() { + return webSocket; + } + + public void setWebSocket(boolean webSocket) { + this.webSocket = webSocket; + } + + public boolean isSsl() { + return ssl; + } + + public void setSsl(boolean ssl) { + this.ssl = ssl; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public Long getConnectedAt() { + return connectedAt; + } + + public void setConnectedAt(Long connectedAt) { + this.connectedAt = connectedAt; + } + + public long getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(long createdAt) { + this.createdAt = createdAt; + } + + public int getDecodeQueueSize() { + return decodeQueueSize; + } + + public void setDecodeQueueSize(int decodeQueueSize) { + this.decodeQueueSize = decodeQueueSize; + } + + public int getHandlerQueueSize() { + return handlerQueueSize; + } + + public void setHandlerQueueSize(int handlerQueueSize) { + this.handlerQueueSize = handlerQueueSize; + } + + public int getSendQueueSize() { + return sendQueueSize; + } + + public void setSendQueueSize(int sendQueueSize) { + this.sendQueueSize = sendQueueSize; + } + + @Override + public String toString() { + return "ClientInfo{" + + "clientId='" + clientId + '\'' + + ", username='" + username + '\'' + + ", keepAlive=" + keepAlive + + ", connected=" + connected + + ", protoName='" + protoName + '\'' + + ", protoVer=" + protoVer + + ", protoFullName='" + protoFullName + '\'' + + ", webSocket=" + webSocket + + ", ssl=" + ssl + + ", ipAddress='" + ipAddress + '\'' + + ", port=" + port + + ", connectedAt=" + connectedAt + + ", createdAt=" + createdAt + + ", decodeQueueSize=" + decodeQueueSize + + ", handlerQueueSize=" + handlerQueueSize + + ", sendQueueSize=" + sendQueueSize + + '}'; + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/Message.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/Message.java new file mode 100644 index 0000000..ec6aab5 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/Message.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.model; + +import org.dromara.mica.mqtt.core.server.enums.MessageType; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Objects; + +/** + * 消息模型,用于存储 + * + * @author L.cm + */ +public class Message implements Serializable { + + /** + * 事件触发所在节点 + */ + private String node; + /** + * MQTT 消息 ID + */ + private Integer id; + /** + * 消息来源 客户端 id + */ + private String fromClientId; + /** + * 消息来源 用户名 + */ + private String fromUsername; + /** + * 消息目的 Client ID,主要是在遗嘱消息用 + */ + private String clientId; + /** + * 消息目的用户名,主要是在遗嘱消息用 + */ + private String username; + /** + * topic + */ + private String topic; + /** + * 消息类型 + */ + private MessageType messageType; + /** + * 是否重发 + */ + private boolean dup; + /** + * qos + */ + private int qos; + /** + * retain + */ + private boolean retain; + /** + * 消息内容 + */ + private byte[] payload; + /** + * 客户端的 IPAddress + */ + private String peerHost; + /** + * 存储时间 + */ + private long timestamp; + /** + * PUBLISH 消息到达 Broker 的时间 (ms) + */ + private Long publishReceivedAt; + + public String getNode() { + return node; + } + + public void setNode(String node) { + this.node = node; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getFromClientId() { + return fromClientId; + } + + public void setFromClientId(String fromClientId) { + this.fromClientId = fromClientId; + } + + public String getFromUsername() { + return fromUsername; + } + + public void setFromUsername(String fromUsername) { + this.fromUsername = fromUsername; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public MessageType getMessageType() { + return messageType; + } + + public void setMessageType(MessageType messageType) { + this.messageType = messageType; + } + + public boolean isDup() { + return dup; + } + + public void setDup(boolean dup) { + this.dup = dup; + } + + public int getQos() { + return qos; + } + + public void setQos(int qos) { + this.qos = qos; + } + + public boolean isRetain() { + return retain; + } + + public void setRetain(boolean retain) { + this.retain = retain; + } + + public byte[] getPayload() { + return payload; + } + + public void setPayload(byte[] payload) { + this.payload = payload; + } + + public String getPeerHost() { + return peerHost; + } + + public void setPeerHost(String peerHost) { + this.peerHost = peerHost; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public Long getPublishReceivedAt() { + return publishReceivedAt; + } + + public void setPublishReceivedAt(Long publishReceivedAt) { + this.publishReceivedAt = publishReceivedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Message message = (Message) o; + return dup == message.dup && qos == message.qos && retain == message.retain && timestamp == message.timestamp && Objects.equals(node, message.node) && Objects.equals(id, message.id) && Objects.equals(fromClientId, message.fromClientId) && Objects.equals(fromUsername, message.fromUsername) && Objects.equals(clientId, message.clientId) && Objects.equals(username, message.username) && Objects.equals(topic, message.topic) && messageType == message.messageType && Arrays.equals(payload, message.payload) && Objects.equals(peerHost, message.peerHost) && Objects.equals(publishReceivedAt, message.publishReceivedAt); + } + + @Override + public int hashCode() { + return Objects.hash(node, id, fromClientId, fromUsername, clientId, username, topic, messageType, dup, qos, retain, Arrays.hashCode(payload), peerHost, timestamp, publishReceivedAt); + } + + @Override + public String toString() { + return "Message{" + + "node='" + node + '\'' + + ", id=" + id + + ", fromClientId='" + fromClientId + '\'' + + ", fromUsername='" + fromUsername + '\'' + + ", clientId='" + clientId + '\'' + + ", username='" + username + '\'' + + ", topic='" + topic + '\'' + + ", messageType=" + messageType + + ", dup=" + dup + + ", qos=" + qos + + ", retain=" + retain + + ", payload=" + payload + + ", peerHost='" + peerHost + '\'' + + ", timestamp=" + timestamp + + ", publishReceivedAt=" + publishReceivedAt + + '}'; + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/Subscribe.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/Subscribe.java new file mode 100644 index 0000000..fb08edf --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/Subscribe.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.model; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 订阅模型,用于存储 + * + * @author L.cm + */ +public class Subscribe implements Serializable { + private String topicFilter; + private String clientId; + private int mqttQoS; + + public Subscribe() { + } + + public Subscribe(String clientId, int mqttQoS) { + this.clientId = clientId; + this.mqttQoS = mqttQoS; + } + + public Subscribe(String topicFilter, String clientId, int mqttQoS) { + this.topicFilter = topicFilter; + this.clientId = clientId; + this.mqttQoS = mqttQoS; + } + + public String getTopicFilter() { + return topicFilter; + } + + public void setTopicFilter(String topicFilter) { + this.topicFilter = topicFilter; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public int getMqttQoS() { + return mqttQoS; + } + + public void setMqttQoS(int mqttQoS) { + this.mqttQoS = mqttQoS; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Subscribe)) { + return false; + } + Subscribe subscribe = (Subscribe) o; + return Objects.equals(topicFilter, subscribe.topicFilter) && + Objects.equals(clientId, subscribe.clientId); + } + + @Override + public int hashCode() { + return Objects.hash(topicFilter, clientId); + } + + @Override + public String toString() { + return "Subscribe{" + + "topicFilter='" + topicFilter + '\'' + + ", clientId='" + clientId + '\'' + + ", mqttQoS=" + mqttQoS + + '}'; + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/protocol/MqttProtocol.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/protocol/MqttProtocol.java new file mode 100644 index 0000000..0d361a8 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/protocol/MqttProtocol.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.protocol; + +/** + * mqtt 协议 + * + * @author L.cm + */ +public enum MqttProtocol { + + /** + * mqtt 协议 + */ + MQTT(1883), + /** + * mqtt ssl 协议 + */ + MQTT_SSL(8883), + /** + * mqtt websocket 子协议 + */ + MQTT_WS(8083), + /** + * mqtt websocket ssl 子协议 + */ + MQTT_WSS(8084), + /** + * mqtt http api 接口 + */ + MQTT_HTTP_API(18083), + ; + + private final int port; + + MqttProtocol(int port) { + this.port = port; + } + + public int getPort() { + return this.port; + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/DefaultMessageSerializer.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/DefaultMessageSerializer.java new file mode 100644 index 0000000..2e659d1 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/DefaultMessageSerializer.java @@ -0,0 +1,861 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.serializer; + +import org.dromara.mica.mqtt.core.server.enums.MessageType; +import org.dromara.mica.mqtt.core.server.model.Message; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + * mica mqtt 消息序列化 + * + * @author L.cm + */ +public enum DefaultMessageSerializer implements org.dromara.mica.mqtt.core.server.serializer.IMessageSerializer { + + /** + * 单利 + */ + INSTANCE; + + /** + * 空 byte 数组 + */ + private static final byte[] EMPTY_BYTES = new byte[0]; + /** + * 空 short byte 数组,2 个长度 + */ + private static final byte[] EMPTY_SHORT_BYTES = new byte[2]; + /** + * 空 int byte 数组,4 个长度 + */ + private static final byte[] EMPTY_INT_BYTES = new byte[4]; + /** + * 空 long byte 数组,8 个长度 + */ + private static final byte[] EMPTY_LONG_BYTES = new byte[8]; + + @Override + public byte[] serialize(Message message) { + if (message == null) { + return EMPTY_BYTES; + } + MessageType messageType = message.getMessageType(); + if (messageType == null) { + throw new IllegalArgumentException("message type is null."); + } + switch (messageType) { + case CONNECT: + case DISCONNECT: + return serializeConnect(messageType, message); + case SUBSCRIBE: + return serializeSubscribe(messageType, message); + case UNSUBSCRIBE: + return serializeUnsubscribe(messageType, message); + case UP_STREAM: + return serializeUpStream(messageType, message); + case DOWN_STREAM: + return serializeDownStream(messageType, message); + default: + throw new IllegalArgumentException("unknown message type: " + messageType); + } + } + + private static byte[] serializeConnect(MessageType messageType, Message message) { + // messageType, node, clientId, username, peerHost, timestamp + // 1 + 2 * 3 + 1 + 8 + int protocolLength = 16; + // 事件触发所在节点 + String node = message.getNode(); + byte[] nodeBytes = null; + if (node != null) { + nodeBytes = node.getBytes(StandardCharsets.UTF_8); + protocolLength += nodeBytes.length; + } + // 消息目的 Client ID,主要是在遗嘱消息用 + String clientId = message.getClientId(); + byte[] clientIdBytes = null; + if (clientId != null) { + clientIdBytes = clientId.getBytes(StandardCharsets.UTF_8); + protocolLength += clientIdBytes.length; + } + // 消息目的用户名,主要是在遗嘱消息用 + String username = message.getUsername(); + byte[] usernameBytes = null; + if (username != null) { + usernameBytes = username.getBytes(StandardCharsets.UTF_8); + protocolLength += usernameBytes.length; + } + // 客户端的 IPAddress + String peerHost = message.getPeerHost(); + byte[] peerHostBytes = null; + if (peerHost != null) { + peerHostBytes = peerHost.getBytes(StandardCharsets.UTF_8); + protocolLength += peerHostBytes.length; + } + ByteBuffer buffer = ByteBuffer.allocate(protocolLength); + // 消息类型 + buffer.put((byte) messageType.getValue()); + // 事件触发所在节点 + if (nodeBytes != null) { + buffer.putShort((short) nodeBytes.length); + buffer.put(nodeBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 消息目的 Client ID,主要是在遗嘱消息用 + if (clientIdBytes != null) { + buffer.putShort((short) clientIdBytes.length); + buffer.put(clientIdBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 消息来源 用户名 + if (usernameBytes != null) { + buffer.putShort((short) usernameBytes.length); + buffer.put(usernameBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 客户端的 IPAddress + if (peerHostBytes != null) { + buffer.put((byte) peerHostBytes.length); + buffer.put(peerHostBytes); + } else { + buffer.put(EMPTY_BYTES); + } + // 存储时间 + buffer.putLong(message.getTimestamp()); + return buffer.array(); + } + + private static byte[] serializeSubscribe(MessageType messageType, Message message) { + // messageType, node, clientId, topic, qos, peerHost, timestamp + // 1 + 2 * 3 + 1 + 1 + 8 + int protocolLength = 17; + // 事件触发所在节点 + String node = message.getNode(); + byte[] nodeBytes = null; + if (node != null) { + nodeBytes = node.getBytes(StandardCharsets.UTF_8); + protocolLength += nodeBytes.length; + } + // 消息目的 Client ID,主要是在遗嘱消息用 + String clientId = message.getClientId(); + byte[] clientIdBytes = null; + if (clientId != null) { + clientIdBytes = clientId.getBytes(StandardCharsets.UTF_8); + protocolLength += clientIdBytes.length; + } + // topic + String topic = message.getTopic(); + byte[] topicBytes = null; + if (topic != null) { + topicBytes = topic.getBytes(StandardCharsets.UTF_8); + protocolLength += topicBytes.length; + } + // 客户端的 IPAddress + String peerHost = message.getPeerHost(); + byte[] peerHostBytes = null; + if (peerHost != null) { + peerHostBytes = peerHost.getBytes(StandardCharsets.UTF_8); + protocolLength += peerHostBytes.length; + } + ByteBuffer buffer = ByteBuffer.allocate(protocolLength); + // 消息类型 + buffer.put((byte) messageType.getValue()); + // 事件触发所在节点 + if (nodeBytes != null) { + buffer.putShort((short) nodeBytes.length); + buffer.put(nodeBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 消息目的 Client ID,主要是在遗嘱消息用 + if (clientIdBytes != null) { + buffer.putShort((short) clientIdBytes.length); + buffer.put(clientIdBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // topic + if (topicBytes != null) { + buffer.putShort((short) topicBytes.length); + buffer.put(topicBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // qos + buffer.put((byte) message.getQos()); + // 客户端的 IPAddress + if (peerHostBytes != null) { + buffer.put((byte) peerHostBytes.length); + buffer.put(peerHostBytes); + } else { + buffer.put(EMPTY_BYTES); + } + // 存储时间 + buffer.putLong(message.getTimestamp()); + return buffer.array(); + } + + private static byte[] serializeUnsubscribe(MessageType messageType, Message message) { + // messageType, node, clientId, topic, peerHost, timestamp + // 1 + 2 * 3 + 1 + 8 + int protocolLength = 16; + // 事件触发所在节点 + String node = message.getNode(); + byte[] nodeBytes = null; + if (node != null) { + nodeBytes = node.getBytes(StandardCharsets.UTF_8); + protocolLength += nodeBytes.length; + } + // 消息目的 Client ID,主要是在遗嘱消息用 + String clientId = message.getClientId(); + byte[] clientIdBytes = null; + if (clientId != null) { + clientIdBytes = clientId.getBytes(StandardCharsets.UTF_8); + protocolLength += clientIdBytes.length; + } + // topic + String topic = message.getTopic(); + byte[] topicBytes = null; + if (topic != null) { + topicBytes = topic.getBytes(StandardCharsets.UTF_8); + protocolLength += topicBytes.length; + } + // 客户端的 IPAddress + String peerHost = message.getPeerHost(); + byte[] peerHostBytes = null; + if (peerHost != null) { + peerHostBytes = peerHost.getBytes(StandardCharsets.UTF_8); + protocolLength += peerHostBytes.length; + } + ByteBuffer buffer = ByteBuffer.allocate(protocolLength); + // 消息类型 + buffer.put((byte) messageType.getValue()); + // 事件触发所在节点 + if (nodeBytes != null) { + buffer.putShort((short) nodeBytes.length); + buffer.put(nodeBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 消息目的 Client ID,主要是在遗嘱消息用 + if (clientIdBytes != null) { + buffer.putShort((short) clientIdBytes.length); + buffer.put(clientIdBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // topic + if (topicBytes != null) { + buffer.putShort((short) topicBytes.length); + buffer.put(topicBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 客户端的 IPAddress + if (peerHostBytes != null) { + buffer.put((byte) peerHostBytes.length); + buffer.put(peerHostBytes); + } else { + buffer.put(EMPTY_BYTES); + } + // 存储时间 + buffer.putLong(message.getTimestamp()); + return buffer.array(); + } + + private static byte[] serializeUpStream(MessageType messageType, Message message) { + // 1 + 2 + 2 + 2 * 5 + 1 + 4 + 1 + 8 + 8 + int protocolLength = 37; + String fromClientId = message.getFromClientId(); + // 消息来源 客户端 id + byte[] fromClientIdBytes = null; + if (fromClientId != null) { + fromClientIdBytes = fromClientId.getBytes(StandardCharsets.UTF_8); + protocolLength += fromClientIdBytes.length; + } + // 消息来源 用户名 + String fromUsername = message.getFromUsername(); + // 消息来源 客户端 id + byte[] fromUsernameBytes = null; + if (fromUsername != null) { + fromUsernameBytes = fromUsername.getBytes(StandardCharsets.UTF_8); + protocolLength += fromUsernameBytes.length; + } + // 消息目的 Client ID,主要是在遗嘱消息用 + String clientId = message.getClientId(); + byte[] clientIdBytes = null; + if (clientId != null) { + clientIdBytes = clientId.getBytes(StandardCharsets.UTF_8); + protocolLength += clientIdBytes.length; + } + // 消息目的用户名,主要是在遗嘱消息用 + String username = message.getUsername(); + byte[] usernameBytes = null; + if (username != null) { + usernameBytes = username.getBytes(StandardCharsets.UTF_8); + protocolLength += usernameBytes.length; + } + // topic + String topic = message.getTopic(); + byte[] topicBytes = null; + if (topic != null) { + topicBytes = topic.getBytes(StandardCharsets.UTF_8); + protocolLength += topicBytes.length; + } + // 消息内容 + byte[] payload = message.getPayload(); + if (payload != null) { + protocolLength += payload.length; + } + // 客户端的 IPAddress + String peerHost = message.getPeerHost(); + byte[] peerHostBytes = null; + if (peerHost != null) { + peerHostBytes = peerHost.getBytes(StandardCharsets.UTF_8); + protocolLength += peerHostBytes.length; + } + // 事件触发所在节点 + String node = message.getNode(); + byte[] nodeBytes = null; + if (node != null) { + nodeBytes = node.getBytes(StandardCharsets.UTF_8); + protocolLength += nodeBytes.length; + } + ByteBuffer buffer = ByteBuffer.allocate(protocolLength); + // 消息类型 + buffer.put((byte) messageType.getValue()); + // 事件触发所在节点 + if (nodeBytes != null) { + buffer.putShort((short) nodeBytes.length); + buffer.put(nodeBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // MQTT 消息 ID + Integer messageId = message.getId(); + if (messageId != null) { + buffer.putShort(messageId.shortValue()); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 消息来源 客户端 id + if (fromClientIdBytes != null) { + buffer.putShort((short) fromClientIdBytes.length); + buffer.put(fromClientIdBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 消息来源 用户名 + if (fromUsernameBytes != null) { + buffer.putShort((short) fromUsernameBytes.length); + buffer.put(fromUsernameBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 消息目的 Client ID,主要是在遗嘱消息用 + if (clientIdBytes != null) { + buffer.putShort((short) clientIdBytes.length); + buffer.put(clientIdBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 消息来源 用户名 + if (usernameBytes != null) { + buffer.putShort((short) usernameBytes.length); + buffer.put(usernameBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // topic + if (topicBytes != null) { + buffer.putShort((short) topicBytes.length); + buffer.put(topicBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // dup、qos、retain + int byte1 = 0; + if (message.isDup()) { + byte1 |= 0x08; + } + byte1 |= message.getQos() << 1; + if (message.isRetain()) { + byte1 |= 0x01; + } + buffer.put((byte) byte1); + // 消息内容 + if (payload != null) { + buffer.putInt(payload.length); + buffer.put(payload); + } else { + buffer.put(EMPTY_INT_BYTES); + } + // 客户端的 IPAddress + if (peerHostBytes != null) { + buffer.put((byte) peerHostBytes.length); + buffer.put(peerHostBytes); + } else { + buffer.put(EMPTY_BYTES); + } + // 存储时间 + buffer.putLong(message.getTimestamp()); + // PUBLISH 消息到达 Broker 的时间 (ms) + Long publishReceivedAt = message.getPublishReceivedAt(); + if (publishReceivedAt != null) { + buffer.putLong(publishReceivedAt); + } else { + buffer.put(EMPTY_LONG_BYTES); + } + return buffer.array(); + } + + private static byte[] serializeDownStream(MessageType messageType, Message message) { + // 1 + 2 + 2 + 2 * 3 + 1 + 4 + 1 + 8 + 8 + int protocolLength = 33; + // 事件触发所在节点 + String node = message.getNode(); + byte[] nodeBytes = null; + if (node != null) { + nodeBytes = node.getBytes(StandardCharsets.UTF_8); + protocolLength += nodeBytes.length; + } + // 消息目的 Client ID,主要是在遗嘱消息用 + String clientId = message.getClientId(); + byte[] clientIdBytes = null; + if (clientId != null) { + clientIdBytes = clientId.getBytes(StandardCharsets.UTF_8); + protocolLength += clientIdBytes.length; + } + // 消息目的用户名,主要是在遗嘱消息用 + String username = message.getUsername(); + byte[] usernameBytes = null; + if (username != null) { + usernameBytes = username.getBytes(StandardCharsets.UTF_8); + protocolLength += usernameBytes.length; + } + // topic + String topic = message.getTopic(); + byte[] topicBytes = null; + if (topic != null) { + topicBytes = topic.getBytes(StandardCharsets.UTF_8); + protocolLength += topicBytes.length; + } + // 消息内容 + byte[] payload = message.getPayload(); + if (payload != null) { + protocolLength += payload.length; + } + // 客户端的 IPAddress + String peerHost = message.getPeerHost(); + byte[] peerHostBytes = null; + if (peerHost != null) { + peerHostBytes = peerHost.getBytes(StandardCharsets.UTF_8); + protocolLength += peerHostBytes.length; + } + ByteBuffer buffer = ByteBuffer.allocate(protocolLength); + // 消息类型 + buffer.put((byte) messageType.getValue()); + // 事件触发所在节点 + if (nodeBytes != null) { + buffer.putShort((short) nodeBytes.length); + buffer.put(nodeBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // MQTT 消息 ID + Integer messageId = message.getId(); + if (messageId != null) { + buffer.putShort(messageId.shortValue()); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 消息目的 Client ID,主要是在遗嘱消息用 + if (clientIdBytes != null) { + buffer.putShort((short) clientIdBytes.length); + buffer.put(clientIdBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // 消息来源 用户名 + if (usernameBytes != null) { + buffer.putShort((short) usernameBytes.length); + buffer.put(usernameBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // topic + if (topicBytes != null) { + buffer.putShort((short) topicBytes.length); + buffer.put(topicBytes); + } else { + buffer.put(EMPTY_SHORT_BYTES); + } + // dup、qos、retain + int byte1 = 0; + if (message.isDup()) { + byte1 |= 0x08; + } + byte1 |= message.getQos() << 1; + if (message.isRetain()) { + byte1 |= 0x01; + } + buffer.put((byte) byte1); + // 消息内容 + if (payload != null) { + buffer.putInt(payload.length); + buffer.put(payload); + } else { + buffer.put(EMPTY_INT_BYTES); + } + // 客户端的 IPAddress + if (peerHostBytes != null) { + buffer.put((byte) peerHostBytes.length); + buffer.put(peerHostBytes); + } else { + buffer.put(EMPTY_BYTES); + } + // 存储时间 + buffer.putLong(message.getTimestamp()); + // PUBLISH 消息到达 Broker 的时间 (ms) + Long publishReceivedAt = message.getPublishReceivedAt(); + if (publishReceivedAt != null) { + buffer.putLong(publishReceivedAt); + } else { + buffer.put(EMPTY_LONG_BYTES); + } + return buffer.array(); + } + + @Override + public Message deserialize(byte[] data) { + // 1. null 或者空 byte 数组 + if (data == null || data.length < 1) { + return null; + } + Message message = new Message(); + ByteBuffer buffer = ByteBuffer.wrap(data); + byte messageTypeByte = buffer.get(); + MessageType messageType = MessageType.valueOf(messageTypeByte); + message.setMessageType(messageType); + switch (messageType) { + case CONNECT: + case DISCONNECT: + return deserializeConnect(buffer, message); + case SUBSCRIBE: + return deserializeSubscribe(buffer, message); + case UNSUBSCRIBE: + return deserializeUnsubscribe(buffer, message); + case UP_STREAM: + return deserializeUpStream(buffer, message); + case DOWN_STREAM: + return deserializeDownStream(buffer, message); + default: + throw new IllegalArgumentException("unknown message type: " + messageType); + } + } + + private static Message deserializeConnect(ByteBuffer buffer, Message message) { + // 事件触发所在节点 + short nodeLength = buffer.getShort(); + if (nodeLength > 0) { + byte[] nodeBytes = new byte[nodeLength]; + buffer.get(nodeBytes); + message.setNode(new String(nodeBytes, StandardCharsets.UTF_8)); + } + // 消息目的 Client ID,主要是在遗嘱消息用 + short clientIdLen = buffer.getShort(); + if (clientIdLen > 0) { + byte[] clientIdBytes = new byte[clientIdLen]; + buffer.get(clientIdBytes); + message.setClientId(new String(clientIdBytes, StandardCharsets.UTF_8)); + } + // 消息目的用户名,主要是在遗嘱消息用 + short usernameLen = buffer.getShort(); + if (usernameLen > 0) { + byte[] usernameBytes = new byte[usernameLen]; + buffer.get(usernameBytes); + message.setUsername(new String(usernameBytes, StandardCharsets.UTF_8)); + } + // 客户端的 peerHost IPAddress + byte peerHostLen = buffer.get(); + if (peerHostLen > 0) { + byte[] peerHostBytes = new byte[peerHostLen]; + buffer.get(peerHostBytes); + message.setPeerHost(new String(peerHostBytes, StandardCharsets.UTF_8)); + } + // 存储时间 + long timestamp = buffer.getLong(); + message.setTimestamp(timestamp); + return message; + } + + private static Message deserializeSubscribe(ByteBuffer buffer, Message message) { + // 事件触发所在节点 + short nodeLength = buffer.getShort(); + if (nodeLength > 0) { + byte[] nodeBytes = new byte[nodeLength]; + buffer.get(nodeBytes); + message.setNode(new String(nodeBytes, StandardCharsets.UTF_8)); + } + // 消息目的 Client ID,主要是在遗嘱消息用 + short clientIdLen = buffer.getShort(); + if (clientIdLen > 0) { + byte[] clientIdBytes = new byte[clientIdLen]; + buffer.get(clientIdBytes); + message.setClientId(new String(clientIdBytes, StandardCharsets.UTF_8)); + } + // topic + short topicLength = buffer.getShort(); + if (topicLength > 0) { + byte[] topicBytes = new byte[topicLength]; + buffer.get(topicBytes); + message.setTopic(new String(topicBytes, StandardCharsets.UTF_8)); + } + byte qos = buffer.get(); + message.setQos(qos); + // 客户端的 peerHost IPAddress + byte peerHostLen = buffer.get(); + if (peerHostLen > 0) { + byte[] peerHostBytes = new byte[peerHostLen]; + buffer.get(peerHostBytes); + message.setPeerHost(new String(peerHostBytes, StandardCharsets.UTF_8)); + } + // 存储时间 + long timestamp = buffer.getLong(); + message.setTimestamp(timestamp); + return message; + } + + private static Message deserializeUnsubscribe(ByteBuffer buffer, Message message) { + // 事件触发所在节点 + short nodeLength = buffer.getShort(); + if (nodeLength > 0) { + byte[] nodeBytes = new byte[nodeLength]; + buffer.get(nodeBytes); + message.setNode(new String(nodeBytes, StandardCharsets.UTF_8)); + } + // 消息目的 Client ID,主要是在遗嘱消息用 + short clientIdLen = buffer.getShort(); + if (clientIdLen > 0) { + byte[] clientIdBytes = new byte[clientIdLen]; + buffer.get(clientIdBytes); + message.setClientId(new String(clientIdBytes, StandardCharsets.UTF_8)); + } + // topic + short topicLength = buffer.getShort(); + if (topicLength > 0) { + byte[] topicBytes = new byte[topicLength]; + buffer.get(topicBytes); + message.setTopic(new String(topicBytes, StandardCharsets.UTF_8)); + } + // 客户端的 peerHost IPAddress + byte peerHostLen = buffer.get(); + if (peerHostLen > 0) { + byte[] peerHostBytes = new byte[peerHostLen]; + buffer.get(peerHostBytes); + message.setPeerHost(new String(peerHostBytes, StandardCharsets.UTF_8)); + } + // 存储时间 + long timestamp = buffer.getLong(); + message.setTimestamp(timestamp); + return message; + } + + private static Message deserializeUpStream(ByteBuffer buffer, Message message) { + // 事件触发所在节点 + short nodeLength = buffer.getShort(); + if (nodeLength > 0) { + byte[] nodeBytes = new byte[nodeLength]; + buffer.get(nodeBytes); + message.setNode(new String(nodeBytes, StandardCharsets.UTF_8)); + } + // MQTT 消息 ID + int messageId = getMessageId(buffer); + if (messageId > 0) { + message.setId(messageId); + } + // 消息来源 客户端 id + short fromClientIdLen = buffer.getShort(); + if (fromClientIdLen > 0) { + byte[] fromClientIdBytes = new byte[fromClientIdLen]; + buffer.get(fromClientIdBytes); + message.setFromClientId(new String(fromClientIdBytes, StandardCharsets.UTF_8)); + } + // 消息来源 用户名 + short fromUsernameLen = buffer.getShort(); + if (fromUsernameLen > 0) { + byte[] fromUsernameBytes = new byte[fromUsernameLen]; + buffer.get(fromUsernameBytes); + message.setFromUsername(new String(fromUsernameBytes, StandardCharsets.UTF_8)); + } + // 消息目的 Client ID,主要是在遗嘱消息用 + short clientIdLen = buffer.getShort(); + if (clientIdLen > 0) { + byte[] clientIdBytes = new byte[clientIdLen]; + buffer.get(clientIdBytes); + message.setClientId(new String(clientIdBytes, StandardCharsets.UTF_8)); + } + // 消息目的用户名,主要是在遗嘱消息用 + short usernameLen = buffer.getShort(); + if (usernameLen > 0) { + byte[] usernameBytes = new byte[usernameLen]; + buffer.get(usernameBytes); + message.setUsername(new String(usernameBytes, StandardCharsets.UTF_8)); + } + // topic + short topicLength = buffer.getShort(); + if (topicLength > 0) { + byte[] topicBytes = new byte[topicLength]; + buffer.get(topicBytes); + message.setTopic(new String(topicBytes, StandardCharsets.UTF_8)); + } + // 消息类型、dup、qos、retain + short byte1 = readUnsignedByte(buffer); + boolean isDup = (byte1 & 0x08) == 0x08; + message.setDup(isDup); + // qos + int qosLevel = (byte1 & 0x06) >> 1; + message.setQos(qosLevel); + // retain + boolean retain = (byte1 & 0x01) != 0; + message.setRetain(retain); + // 消息内容 + int payloadLen = buffer.getInt(); + if (payloadLen > 0) { + byte[] payloadBytes = new byte[payloadLen]; + buffer.get(payloadBytes); + message.setPayload(payloadBytes); + } + // 客户端的 peerHost IPAddress + byte peerHostLen = buffer.get(); + if (peerHostLen > 0) { + byte[] peerHostBytes = new byte[peerHostLen]; + buffer.get(peerHostBytes); + message.setPeerHost(new String(peerHostBytes, StandardCharsets.UTF_8)); + } + // 存储时间 + long timestamp = buffer.getLong(); + message.setTimestamp(timestamp); + // PUBLISH 消息到达 Broker 的时间 (ms) + long publishReceivedAt = buffer.getLong(); + if (publishReceivedAt > 0) { + message.setPublishReceivedAt(publishReceivedAt); + } + return message; + } + + private static Message deserializeDownStream(ByteBuffer buffer, Message message) { + // 事件触发所在节点 + short nodeLength = buffer.getShort(); + if (nodeLength > 0) { + byte[] nodeBytes = new byte[nodeLength]; + buffer.get(nodeBytes); + message.setNode(new String(nodeBytes, StandardCharsets.UTF_8)); + } + // MQTT 消息 ID + int messageId = getMessageId(buffer); + if (messageId > 0) { + message.setId(messageId); + } + // 消息目的 Client ID,主要是在遗嘱消息用 + short clientIdLen = buffer.getShort(); + if (clientIdLen > 0) { + byte[] clientIdBytes = new byte[clientIdLen]; + buffer.get(clientIdBytes); + message.setClientId(new String(clientIdBytes, StandardCharsets.UTF_8)); + } + // 消息目的用户名,主要是在遗嘱消息用 + short usernameLen = buffer.getShort(); + if (usernameLen > 0) { + byte[] usernameBytes = new byte[usernameLen]; + buffer.get(usernameBytes); + message.setUsername(new String(usernameBytes, StandardCharsets.UTF_8)); + } + // topic + short topicLength = buffer.getShort(); + if (topicLength > 0) { + byte[] topicBytes = new byte[topicLength]; + buffer.get(topicBytes); + message.setTopic(new String(topicBytes, StandardCharsets.UTF_8)); + } + // 消息类型、dup、qos、retain + short byte1 = readUnsignedByte(buffer); + boolean isDup = (byte1 & 0x08) == 0x08; + message.setDup(isDup); + // qos + int qosLevel = (byte1 & 0x06) >> 1; + message.setQos(qosLevel); + // retain + boolean retain = (byte1 & 0x01) != 0; + message.setRetain(retain); + // 消息内容 + int payloadLen = buffer.getInt(); + if (payloadLen > 0) { + byte[] payloadBytes = new byte[payloadLen]; + buffer.get(payloadBytes); + message.setPayload(payloadBytes); + } + // 客户端的 peerHost IPAddress + byte peerHostLen = buffer.get(); + if (peerHostLen > 0) { + byte[] peerHostBytes = new byte[peerHostLen]; + buffer.get(peerHostBytes); + message.setPeerHost(new String(peerHostBytes, StandardCharsets.UTF_8)); + } + // 存储时间 + long timestamp = buffer.getLong(); + message.setTimestamp(timestamp); + // PUBLISH 消息到达 Broker 的时间 (ms) + long publishReceivedAt = buffer.getLong(); + if (publishReceivedAt > 0) { + message.setPublishReceivedAt(publishReceivedAt); + } + return message; + } + + /** + * read unsigned byte + * + * @param buffer ByteBuffer + * @return short + */ + private static short readUnsignedByte(ByteBuffer buffer) { + return (short) (buffer.get() & 0xFF); + } + + /** + * MessageId numberOfBytesConsumed = 2. return decoded result. + */ + private static int getMessageId(ByteBuffer buffer) { + int min = 0; + int max = 65535; + short msbSize = readUnsignedByte(buffer); + short lsbSize = readUnsignedByte(buffer); + int result = msbSize << 8 | lsbSize; + if (result < min || result > max) { + result = -1; + } + return result; + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/IMessageSerializer.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/IMessageSerializer.java new file mode 100644 index 0000000..417870e --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/IMessageSerializer.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.serializer; + +import org.dromara.mica.mqtt.core.server.model.Message; + +/** + * 消息编解码 + * + * @author L.cm + */ +public interface IMessageSerializer { + + /** + * 消息序列化 + * + * @param message Message + * @return byte array + */ + byte[] serialize(Message message); + + /** + * 消息反序列化 + * + * @param data byte array + * @return Message + */ + Message deserialize(byte[] data); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/JsonMessageSerializer.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/JsonMessageSerializer.java new file mode 100644 index 0000000..af04bf4 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/JsonMessageSerializer.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.serializer; + +import org.dromara.mica.mqtt.core.server.model.Message; +import org.tio.utils.json.JsonUtil; + +/** + * fastjson 序列化 + * + * @author L.cm + */ +public class JsonMessageSerializer implements org.dromara.mica.mqtt.core.server.serializer.IMessageSerializer { + + @Override + public byte[] serialize(Message message) { + return JsonUtil.toJsonBytes(message); + } + + @Override + public Message deserialize(byte[] data) { + return JsonUtil.readValue(data, Message.class); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/IMqttSessionManager.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/IMqttSessionManager.java new file mode 100644 index 0000000..83224f6 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/IMqttSessionManager.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.session; + +import org.dromara.mica.mqtt.core.common.MqttPendingPublish; +import org.dromara.mica.mqtt.core.common.MqttPendingQos2Publish; +import org.dromara.mica.mqtt.core.common.TopicFilter; +import org.dromara.mica.mqtt.core.server.model.Subscribe; + +import java.util.List; + +/** + * session 管理,不封装 MqttSession 实体,方便 redis 等集群处理 + * + * @author L.cm + */ +public interface IMqttSessionManager { + + /** + * 添加订阅存储 + * + * @param topicFilter topicFilter + * @param clientId 客户端 Id + * @param mqttQoS MqttQoS + */ + void addSubscribe(TopicFilter topicFilter, String clientId, int mqttQoS); + + /** + * 添加订阅存储 + * + * @param topicFilter topicFilter + * @param clientId 客户端 Id + * @param mqttQoS MqttQoS + */ + default void addSubscribe(String topicFilter, String clientId, int mqttQoS) { + this.addSubscribe(new TopicFilter(topicFilter), clientId, mqttQoS); + } + + /** + * 删除订阅 + * + * @param topicFilter topicFilter + * @param clientId 客户端 Id + */ + void removeSubscribe(String topicFilter, String clientId); + + /** + * 查找订阅 qos 信息 + * + * @param topicName topicName + * @param clientId 客户端 Id + * @return 订阅存储列表 + */ + Byte searchSubscribe(String topicName, String clientId); + + /** + * 查找订阅信息 + * + * @param topicName topicName + * @return 订阅存储列表 + */ + List searchSubscribe(String topicName); + + /** + * 获取设备订阅 + * + * @param clientId clientId + * @return 订阅列表 + */ + List getSubscriptions(String clientId); + + /** + * 添加发布过程存储 + * + * @param clientId clientId + * @param messageId messageId + * @param pendingPublish MqttPendingPublish + */ + void addPendingPublish(String clientId, int messageId, MqttPendingPublish pendingPublish); + + /** + * 获取发布过程存储 + * + * @param clientId clientId + * @param messageId messageId + * @return MqttPendingPublish + */ + MqttPendingPublish getPendingPublish(String clientId, int messageId); + + /** + * 删除发布过程中的存储 + * + * @param clientId clientId + * @param messageId messageId + */ + void removePendingPublish(String clientId, int messageId); + + /** + * 添加发布过程存储 + * + * @param clientId clientId + * @param messageId messageId + * @param pendingQos2Publish MqttPendingQos2Publish + */ + void addPendingQos2Publish(String clientId, int messageId, MqttPendingQos2Publish pendingQos2Publish); + + /** + * 获取发布过程存储 + * + * @param clientId clientId + * @param messageId messageId + * @return MqttPendingQos2Publish + */ + MqttPendingQos2Publish getPendingQos2Publish(String clientId, int messageId); + + /** + * 删除发布过程中的存储 + * + * @param clientId clientId + * @param messageId messageId + */ + void removePendingQos2Publish(String clientId, int messageId); + + /** + * 生成消息 Id + * + * @param clientId clientId + * @return messageId + */ + int getPacketId(String clientId); + + /** + * 判断是否存在 session + * + * @param clientId clientId + * @return 是否存在 session + */ + boolean hasSession(String clientId); + + /** + * 标记 session 超时时间 + * + * @param clientId clientId + * @param sessionExpirySeconds sessionExpirySeconds + * @return 是否成功 + */ + boolean expire(String clientId, int sessionExpirySeconds); + + /** + * 激活 session,标记 expire 的 session 为永久 + * + * @param clientId clientId + * @return 是否成功 + */ + boolean active(String clientId); + + /** + * 清除 session + * + * @param clientId clientId + */ + void remove(String clientId); + + /** + * 清理 + */ + void clean(); + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/InMemoryMqttSessionManager.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/InMemoryMqttSessionManager.java new file mode 100644 index 0000000..a58031a --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/InMemoryMqttSessionManager.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.session; + +import org.dromara.mica.mqtt.core.common.MqttPendingPublish; +import org.dromara.mica.mqtt.core.common.MqttPendingQos2Publish; +import org.dromara.mica.mqtt.core.common.TopicFilter; +import org.dromara.mica.mqtt.core.server.model.Subscribe; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 内存 session 管理 + * + * @author L.cm + */ +public class InMemoryMqttSessionManager implements IMqttSessionManager { + /** + * messageId 存储 clientId: messageId + */ + private final ConcurrentMap messageIdStore = new ConcurrentHashMap<>(); + /** + * 订阅存储,支持共享订阅 + */ + private final TrieTopicManager topicManager = new TrieTopicManager(); + /** + * qos1 消息过程存储 clientId: {msgId: Object} + */ + private final ConcurrentMap> pendingPublishStore = new ConcurrentHashMap<>(); + /** + * qos2 消息过程存储 clientId: {msgId: Object} + */ + private final ConcurrentMap> pendingQos2PublishStore = new ConcurrentHashMap<>(); + + @Override + public void addSubscribe(TopicFilter topicFilter, String clientId, int mqttQoS) { + topicManager.addSubscribe(topicFilter, clientId, (short) mqttQoS); + } + + @Override + public void removeSubscribe(String topicFilter, String clientId) { + topicManager.removeSubscribe(topicFilter, clientId); + } + + public void removeSubscribe(String clientId) { + topicManager.removeSubscribe(clientId); + } + + @Override + public Byte searchSubscribe(String topicName, String clientId) { + return topicManager.searchSubscribe(topicName, clientId); + } + + @Override + public List searchSubscribe(String topicName) { + return topicManager.searchSubscribe(topicName); + } + + @Override + public List getSubscriptions(String clientId) { + return topicManager.getSubscriptions(clientId); + } + + @Override + public void addPendingPublish(String clientId, int messageId, MqttPendingPublish pendingPublish) { + Map data = pendingPublishStore.computeIfAbsent(clientId, (key) -> new ConcurrentHashMap<>(16)); + data.put(messageId, pendingPublish); + } + + @Override + public MqttPendingPublish getPendingPublish(String clientId, int messageId) { + Map data = pendingPublishStore.get(clientId); + if (data == null) { + return null; + } + return data.get(messageId); + } + + @Override + public void removePendingPublish(String clientId, int messageId) { + Map data = pendingPublishStore.get(clientId); + if (data != null) { + data.remove(messageId); + } + } + + @Override + public void addPendingQos2Publish(String clientId, int messageId, MqttPendingQos2Publish pendingQos2Publish) { + Map data = pendingQos2PublishStore.computeIfAbsent(clientId, (key) -> new ConcurrentHashMap<>(16)); + data.put(messageId, pendingQos2Publish); + } + + @Override + public MqttPendingQos2Publish getPendingQos2Publish(String clientId, int messageId) { + Map data = pendingQos2PublishStore.get(clientId); + if (data == null) { + return null; + } + return data.get(messageId); + } + + @Override + public void removePendingQos2Publish(String clientId, int messageId) { + Map data = pendingQos2PublishStore.get(clientId); + if (data != null) { + data.remove(messageId); + } + } + + @Override + public int getPacketId(String clientId) { + AtomicInteger packetIdGen = messageIdStore.computeIfAbsent(clientId, (key) -> new AtomicInteger(1)); + return packetIdGen.getAndUpdate(current -> (current % 0xffff) == 0 ? 1 : current + 1); + } + + @Override + public boolean hasSession(String clientId) { + return pendingQos2PublishStore.containsKey(clientId) + || pendingPublishStore.containsKey(clientId) + || messageIdStore.containsKey(clientId) + || !topicManager.getSubscriptions(clientId).isEmpty(); + } + + @Override + public boolean expire(String clientId, int sessionExpirySeconds) { + return false; + } + + @Override + public boolean active(String clientId) { + return false; + } + + @Override + public void remove(String clientId) { + removeSubscribe(clientId); + pendingPublishStore.remove(clientId); + pendingQos2PublishStore.remove(clientId); + messageIdStore.remove(clientId); + } + + @Override + public void clean() { + topicManager.clear(); + pendingPublishStore.clear(); + pendingQos2PublishStore.clear(); + messageIdStore.clear(); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/SharedStrategy.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/SharedStrategy.java new file mode 100644 index 0000000..b9f77cb --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/SharedStrategy.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.session; + +/** + * 共享订阅均衡策略 + * + * @author L.cm + */ +public enum SharedStrategy { + + /** + * 在所有订阅者中随机选择 + */ + random, + /** + * 按照订阅顺序 + */ + round_robin, + /** + * 一直发往上次选取的订阅者 + */ + sticky, + /** + * 按照发布者 ClientID 的哈希值 + */ + hash; + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/TrieTopicManager.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/TrieTopicManager.java new file mode 100644 index 0000000..377b74a --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/TrieTopicManager.java @@ -0,0 +1,513 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.session; + +import org.dromara.mica.mqtt.codec.MqttCodecUtil; +import org.dromara.mica.mqtt.core.common.TopicFilter; +import org.dromara.mica.mqtt.core.common.TopicFilterType; +import org.dromara.mica.mqtt.core.server.model.Subscribe; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.tio.utils.hutool.CollUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.BinaryOperator; +import java.util.stream.Collectors; + +/** + * 前缀树 + * + * @author L.cm + */ +public class TrieTopicManager { + + /** + * 较大的 qos + */ + public static final BinaryOperator MAX_QOS = (a, b) -> (a > b) ? a : b; + /** + * root 节点 + */ + private final Node root = Node.getRoot("root"); + /** + * share 分组 + */ + private final Map share = new ConcurrentHashMap<>(); + /** + * queue 分组 + */ + private final Node queue = Node.getRoot("$queue"); + + private static class Node { + /** + * topic 片段 + */ + private final String part; + /** + * 订阅的数据存储 {clientId: qos} + */ + private final Map subscriptions; + /** + * 子节点 + */ + private final Map children; + + private Node(String part, Map subscriptions, Map children) { + this.part = part; + this.subscriptions = subscriptions; + this.children = children; + } + + /** + * 获取 root node + * + * @param name name + * @return root node + */ + protected static Node getRoot(String name) { + return new Node(name, null, new ConcurrentHashMap<>(8)); + } + + /** + * 用于存储数据的节点 + * + * @param part part + * @return node + */ + protected static Node getNode(String part) { + return new Node(part, new ConcurrentHashMap<>(16), new ConcurrentHashMap<>(16)); + } + + /** + * 获取或者添加节点 + * + * @param nodePart nodePart + * @return Node + */ + protected Node addChildIfAbsent(String nodePart) { + assert children != null; + return CollUtil.computeIfAbsent(this.children, nodePart, Node::getNode); + } + + protected Node findNodeByPart(String nodePart) { + assert children != null; + return children.get(nodePart); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || Node.class != o.getClass() || part == null) { + return false; + } + return part.equals(((Node) o).part); + } + + @Override + public int hashCode() { + return part == null ? 0 : part.hashCode(); + } + + @Override + public String toString() { + return "Node{" + + "part='" + part + '\'' + + ", subscriptions=" + subscriptions + + ", children=" + children + + '}'; + } + } + + /** + * 添加订阅 + * + * @param topicFilter topicFilter + * @param clientId clientId + * @param mqttQoS mqttQoS + */ + public void addSubscribe(String topicFilter, String clientId, int mqttQoS) { + addSubscribe(new TopicFilter(topicFilter), clientId, (short) mqttQoS); + } + + /** + * 添加订阅 + * + * @param topicFilter topicFilter + * @param clientId clientId + * @param mqttQoS mqttQoS + */ + public void addSubscribe(TopicFilter topicFilter, String clientId, int mqttQoS) { + String topic = topicFilter.getTopic(); + TopicFilterType topicFilterType = topicFilter.getType(); + if (TopicFilterType.NONE == topicFilterType) { + addSubscribe(root, topic, clientId, (byte) mqttQoS); + } else if (TopicFilterType.QUEUE == topicFilterType) { + int prefixLen = TopicFilterType.SHARE_QUEUE_PREFIX.length(); + addSubscribe(queue, topic.substring(prefixLen), clientId, (byte) mqttQoS); + } else if (TopicFilterType.SHARE == topicFilterType) { + int prefixLen = TopicFilterType.SHARE_GROUP_PREFIX.length(); + String groupName = TopicFilterType.getShareGroupName(topic); + Node groupNode = share.computeIfAbsent(groupName, Node::getNode); + prefixLen = prefixLen + groupName.length() + 1; + addSubscribe(groupNode, topic.substring(prefixLen), clientId, (byte) mqttQoS); + } + } + + /** + * 添加订阅 + * + * @param node node + * @param topicFilter topicFilter + * @param clientId clientId + * @param mqttQoS mqttQoS + */ + private static void addSubscribe(Node node, String topicFilter, String clientId, byte mqttQoS) { + Node prev = node; + String[] topicParts = TopicUtil.getTopicParts(topicFilter); + int partLength = topicParts.length - 1; + for (int i = 0; i < topicParts.length; i++) { + prev = prev.addChildIfAbsent(topicParts[i]); + // 判断是否结尾,添加订阅数据 + boolean isEnd = i == partLength; + if (isEnd) { + // 如果不存在或者老的订阅 qos 比较小也重新设置 + assert prev.subscriptions != null; + Byte existingQos = prev.subscriptions.get(clientId); + if (existingQos == null || existingQos < mqttQoS) { + prev.subscriptions.put(clientId, mqttQoS); + } + } + } + } + + /** + * 移除订阅 + * + * @param topicFilter topicFilter + * @param clientId clientId + */ + public void removeSubscribe(String topicFilter, String clientId) { + removeSubscribe(new TopicFilter(topicFilter), clientId); + } + + /** + * 移除订阅 + * + * @param topicFilter topicFilter + * @param clientId clientId + */ + private void removeSubscribe(TopicFilter topicFilter, String clientId) { + String topic = topicFilter.getTopic(); + TopicFilterType topicFilterType = topicFilter.getType(); + if (TopicFilterType.NONE == topicFilterType) { + removeSubscribe(root, topic, clientId); + } else if (TopicFilterType.QUEUE == topicFilterType) { + int prefixLen = TopicFilterType.SHARE_QUEUE_PREFIX.length(); + removeSubscribe(queue, topic.substring(prefixLen), clientId); + } else if (TopicFilterType.SHARE == topicFilterType) { + int prefixLen = TopicFilterType.SHARE_GROUP_PREFIX.length(); + String groupName = TopicFilterType.getShareGroupName(topic); + Node groupNode = share.computeIfAbsent(groupName, Node::getNode); + prefixLen = prefixLen + groupName.length() + 1; + removeSubscribe(groupNode, topic.substring(prefixLen), clientId); + } + } + + /** + * 移除订阅 + * + * @param topicFilter topicFilter + * @param clientId clientId + */ + private static void removeSubscribe(Node node, String topicFilter, String clientId) { + Node prev = node; + String[] topicParts = TopicUtil.getTopicParts(topicFilter); + for (String part : topicParts) { + Node nodePart = prev.findNodeByPart(part); + if (nodePart != null) { + prev = nodePart; + } else { + prev = null; + break; + } + } + // 找到则取消订阅 + if (prev != null) { + assert prev.subscriptions != null; + prev.subscriptions.remove(clientId); + } + } + + /** + * 根据 clientId 删除客户端的所以订阅 + * + * @param clientId clientId + */ + public void removeSubscribe(String clientId) { + removeSubscribe(root, clientId); + removeSubscribe(queue, clientId); + for (Node node : share.values()) { + removeSubscribe(node, clientId); + } + } + + /** + * 根据 clientId 删除客户端的所以订阅 + * + * @param clientId clientId + */ + private static void removeSubscribe(Node node, String clientId) { + assert node.children != null; + for (Node child : node.children.values()) { + removeSubscribeRecursively(child, clientId); + } + } + + /** + * 递归删除订阅 + * + * @param child child + * @param clientId clientId + */ + private static void removeSubscribeRecursively(Node child, String clientId) { + // 删除订阅 + assert child.subscriptions != null; + child.subscriptions.remove(clientId); + assert child.children != null; + for (Node node : child.children.values()) { + removeSubscribeRecursively(node, clientId); + } + } + + /** + * 获取客户端所以订阅 + * + * @param clientId clientId + * @return 订阅集合 + */ + public List getSubscriptions(String clientId) { + List subscribeList = getSubscriptions(root, null, clientId); + subscribeList.addAll(getSubscriptions(queue, TopicFilterType.SHARE_QUEUE_PREFIX, clientId)); + for (Map.Entry entry : share.entrySet()) { + String prefix = TopicFilterType.SHARE_GROUP_PREFIX + entry.getKey() + TopicUtil.TOPIC_LAYER; + subscribeList.addAll(getSubscriptions(entry.getValue(), prefix, clientId)); + } + return subscribeList.stream().distinct().collect(Collectors.toList()); + } + + /** + * 获取客户端所以订阅 + * + * @param clientId clientId + * @return 订阅集合 + */ + private static List getSubscriptions(Node node, String prefix, String clientId) { + List subscribeList = new ArrayList<>(); + for (Node child : node.children.values()) { + String topicPrefix = prefix == null ? child.part : prefix + child.part; + getSubscribeRecursively(subscribeList, child, topicPrefix, clientId); + } + return subscribeList; + } + + /** + * 递归获取订阅 + * + * @param child child + * @param clientId clientId + */ + private static void getSubscribeRecursively(List subscribeList, Node child, String childPart, String clientId) { + // 删除订阅 + assert child.subscriptions != null; + Byte qos = child.subscriptions.get(clientId); + if (qos != null) { + subscribeList.add(new Subscribe(childPart, clientId, qos)); + } + assert child.children != null; + for (Node node : child.children.values()) { + // 拼接订阅的 topic,存储时没存,可以减少内存占用。 + String topicPrefix = isNotNeedAppendTopicLayer(childPart, node.part) ? + childPart + node.part : childPart + MqttCodecUtil.TOPIC_LAYER + node.part; + getSubscribeRecursively(subscribeList, node, topicPrefix, clientId); + } + } + + /** + * 判断是否需要添加层级 + * + * @param prefix prefix + * @param suffix suffix + * @return 是否需要添加层级 + */ + private static boolean isNotNeedAppendTopicLayer(String prefix, String suffix) { + return TopicUtil.TOPIC_LAYER.equals(prefix) || prefix.endsWith("//") || TopicUtil.TOPIC_LAYER.equals(suffix); + } + + /** + * 查找订阅 qos 信息 + * + * @param topicName topicName + * @param clientId 客户端 Id + * @return 订阅存储列表 + */ + public Byte searchSubscribe(String topicName, String clientId) { + String[] topicParts = TopicUtil.getTopicParts(topicName); + Map subscribeMap = new HashMap<>(32); + searchSubscribeRecursively(root, subscribeMap, topicParts, 0); + Byte qos = subscribeMap.get(clientId); + if (qos != null) { + return qos; + } + searchSubscribeRecursively(queue, subscribeMap, topicParts, 0); + qos = subscribeMap.get(clientId); + if (qos != null) { + return qos; + } + // 共享订阅 + for (Node node : share.values()) { + searchSubscribeRecursively(node, subscribeMap, topicParts, 0); + } + return subscribeMap.get(clientId); + } + + /** + * 查找订阅信息 + * + * @param topicName topicName + * @return 订阅存储列表 + */ + public List searchSubscribe(String topicName) { + String[] topicParts = TopicUtil.getTopicParts(topicName); + Map subscribeMap = new HashMap<>(32); + searchSubscribeRecursively(root, subscribeMap, topicParts, 0); + // 共享订阅 + Map queueSubscribeMap = new HashMap<>(8); + searchSubscribeRecursively(queue, queueSubscribeMap, topicParts, 0); + if (!queueSubscribeMap.isEmpty()) { + randomStrategy(subscribeMap, queueSubscribeMap); + } + // 分组订阅 + for (Node node : share.values()) { + Map shareSubscribeMap = new HashMap<>(8); + searchSubscribeRecursively(node, shareSubscribeMap, topicParts, 0); + if (!shareSubscribeMap.isEmpty()) { + randomStrategy(subscribeMap, shareSubscribeMap); + } + } + // 转换,排重 + List subscribeList = new ArrayList<>(); + subscribeMap.forEach((clientId, qos) -> subscribeList.add(new Subscribe(clientId, qos))); + subscribeMap.clear(); + return subscribeList; + } + + /** + * 递归查找 + * + * @param node node + * @param subscribeMap subscribeMap + * @param topicParts topicParts + * @param index index + */ + private static void searchSubscribeRecursively(Node node, Map subscribeMap, String[] topicParts, int index) { + // 层级已经超过,跳出 + if (index >= topicParts.length) { + return; + } + // # 单独处理 + Node nodeMore = node.findNodeByPart(TopicUtil.TOPIC_WILDCARDS_MORE); + if (nodeMore != null) { + for (Map.Entry entry : nodeMore.subscriptions.entrySet()) { + subscribeMap.merge(entry.getKey(), entry.getValue(), MAX_QOS); + } + } + int topicPartLen = topicParts.length - 1; + // + 处理 + Node nodeOne = node.findNodeByPart(TopicUtil.TOPIC_WILDCARDS_ONE); + if (nodeOne != null) { + // 最后一位为 + + if (index == topicPartLen) { + for (Map.Entry entry : nodeOne.subscriptions.entrySet()) { + subscribeMap.merge(entry.getKey(), entry.getValue(), MAX_QOS); + } + } else { + searchSubscribeRecursively(nodeOne, subscribeMap, topicParts, index + 1); + } + } + String topicPart = topicParts[index]; + Node nodePart = node.findNodeByPart(topicPart); + if (nodePart != null) { + // 跳出循环 + if (index == topicPartLen) { + for (Map.Entry entry : nodePart.subscriptions.entrySet()) { + subscribeMap.merge(entry.getKey(), entry.getValue(), MAX_QOS); + } + // 判断是否还有 # + Node nodePartMore = nodePart.findNodeByPart(TopicUtil.TOPIC_WILDCARDS_MORE); + if (nodePartMore != null) { + for (Map.Entry entry : nodePartMore.subscriptions.entrySet()) { + subscribeMap.merge(entry.getKey(), entry.getValue(), MAX_QOS); + } + } + } else { + searchSubscribeRecursively(nodePart, subscribeMap, topicParts, index + 1); + } + } + } + + /** + * 清理 + */ + public void clear() { + // 清理普通订阅 + root.children.clear(); + // 清理共享订阅 + queue.children.clear(); + // 清理分组共享订阅 + share.clear(); + } + + @Override + public String toString() { + return "TrieTopicManager{" + + "root=" + root + + ", share=" + share + + ", queue=" + queue + + '}'; + } + + /** + * 负载均衡策略:随机方式 + * + * @param subscribeMap 订阅的 map + * @param randomSubscribeMap 分组订阅的 map + */ + private static void randomStrategy(Map subscribeMap, Map randomSubscribeMap) { + String[] keys = randomSubscribeMap.keySet().toArray(new String[0]); + int keyLength = keys.length; + // 大于 1 随机 + String key = keyLength > 1 ? keys[ThreadLocalRandom.current().nextInt(keyLength)] : keys[0]; + subscribeMap.merge(key, randomSubscribeMap.get(key), MAX_QOS); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/store/IMqttMessageStore.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/store/IMqttMessageStore.java new file mode 100644 index 0000000..2691c0d --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/store/IMqttMessageStore.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.store; + +import org.dromara.mica.mqtt.core.server.model.Message; + +import java.io.IOException; +import java.util.List; + +/** + * message store + * + * @author L.cm + */ +public interface IMqttMessageStore { + + /** + * 存储 clientId 的遗嘱消息 + * + * @param clientId clientId + * @param message message + * @return boolean + */ + boolean addWillMessage(String clientId, Message message); + + /** + * 清理该 clientId 的遗嘱消息 + * + * @param clientId clientId + * @return boolean + */ + boolean clearWillMessage(String clientId); + + /** + * 获取 will 消息 + * + * @param clientId clientId + * @return Message + */ + Message getWillMessage(String clientId); + + /** + * 存储 retain 消息 + * + * @param topic topic + * @param timeout timeout + * @param message message + * @return boolean + */ + boolean addRetainMessage(String topic, int timeout, Message message); + + /** + * 清理该 topic 的 retain 消息 + * + * @param topic topic + * @return boolean + */ + boolean clearRetainMessage(String topic); + + /** + * 获取所有 retain 消息 + * + * @param topicFilter topicFilter + * @return Message + */ + List getRetainMessage(String topicFilter); + + /** + * 清除所有,并释放资源 + */ + default void clean() throws IOException { + + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/store/InMemoryMqttMessageStore.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/store/InMemoryMqttMessageStore.java new file mode 100644 index 0000000..59acbe7 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/store/InMemoryMqttMessageStore.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.store; + + +import org.dromara.mica.mqtt.core.server.model.Message; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.tio.utils.cache.TimedCache; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +/** + * message store + * + * @author L.cm + */ +public class InMemoryMqttMessageStore implements IMqttMessageStore { + /** + * 遗嘱消息 clientId: Message + */ + private final ConcurrentMap willStore = new ConcurrentHashMap<>(); + /** + * 保持消息 topic: Message + * 带有有效期的保持消息 topic: Message + */ + private final ConcurrentMap retainStore = new ConcurrentHashMap<>(); + /** + * 带有有效期的保持消息 topic: Message + */ + private final TimedCache timedRetainStore = new TimedCache<>( + TimeUnit.HOURS.toMillis(2), // 默认 2 小时缓存 + TimeUnit.SECONDS.toMillis(1), // 定时 1s 清理一次缓存 + new ConcurrentHashMap<>() + ); + + @Override + public boolean addWillMessage(String clientId, Message message) { + willStore.put(clientId, message); + return true; + } + + @Override + public boolean clearWillMessage(String clientId) { + willStore.remove(clientId); + return true; + } + + @Override + public Message getWillMessage(String clientId) { + return willStore.get(clientId); + } + + @Override + public boolean addRetainMessage(String topic, int timeout, Message message) { + if (timeout <= 0) { + retainStore.put(topic, message); + } else { + timedRetainStore.put(topic, message, TimeUnit.SECONDS.toMillis(timeout)); + } + return true; + } + + @Override + public boolean clearRetainMessage(String topic) { + retainStore.remove(topic); + timedRetainStore.remove(topic); + return true; + } + + @Override + public List getRetainMessage(String topicFilter) { + List retainMessageList = new ArrayList<>(); + for (String topic : retainStore.keySet()) { + if (TopicUtil.match(topicFilter, topic)) { + retainMessageList.add(retainStore.get(topic)); + } + } + for (String topic : timedRetainStore.keySet()) { + if (TopicUtil.match(topicFilter, topic)) { + retainMessageList.add(timedRetainStore.get(topic)); + } + } + return retainMessageList; + } + + @Override + public void clean() throws IOException { + this.willStore.clear(); + this.retainStore.clear(); + this.timedRetainStore.clear(); + this.timedRetainStore.close(); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttConnectStatusListener.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttConnectStatusListener.java new file mode 100644 index 0000000..8d37f15 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttConnectStatusListener.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.support; + +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +/** + * 默认的链接状态监听 + * + * @author L.cm + */ +public class DefaultMqttConnectStatusListener implements IMqttConnectStatusListener { + private static final Logger logger = LoggerFactory.getLogger(DefaultMqttConnectStatusListener.class); + + @Override + public void online(ChannelContext context, String clientId, String username) { + logger.info("Mqtt clientId:{} username:{} online.", clientId, username); + } + + @Override + public void offline(ChannelContext context, String clientId, String username, String reason) { + logger.info("Mqtt clientId:{} username:{} offline reason:{}.", clientId, username, reason); + } +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerAuthHandler.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerAuthHandler.java new file mode 100644 index 0000000..3dd594e --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerAuthHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.support; + +import org.dromara.mica.mqtt.core.server.auth.IMqttServerAuthHandler; +import org.tio.core.ChannelContext; + +import java.util.Objects; + +/** + * 默认的认证处理 + * + * @author L.cm + */ +public class DefaultMqttServerAuthHandler implements IMqttServerAuthHandler { + private final String authUsername; + private final String authPassword; + + public DefaultMqttServerAuthHandler(String authUsername, String authPassword) { + this.authUsername = Objects.requireNonNull(authUsername, "Mqtt auth enabled but username is null."); + this.authPassword = Objects.requireNonNull(authPassword, "Mqtt auth enabled but password is null."); + } + + @Override + public boolean authenticate(ChannelContext context, String uniqueId, String clientId, String username, String password) { + return authUsername.equals(username) && authPassword.equals(password); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerProcessor.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerProcessor.java new file mode 100644 index 0000000..2a9d67f --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerProcessor.java @@ -0,0 +1,545 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.support; + +import org.dromara.mica.mqtt.codec.MqttMessageFactory; +import org.dromara.mica.mqtt.codec.MqttMessageType; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.codes.MqttConnectReasonCode; +import org.dromara.mica.mqtt.codec.message.*; +import org.dromara.mica.mqtt.codec.message.builder.MqttTopicSubscription; +import org.dromara.mica.mqtt.codec.message.header.MqttConnectVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttFixedHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttMessageIdVariableHeader; +import org.dromara.mica.mqtt.codec.message.header.MqttPublishVariableHeader; +import org.dromara.mica.mqtt.codec.message.payload.MqttConnectPayload; +import org.dromara.mica.mqtt.core.common.MqttPendingPublish; +import org.dromara.mica.mqtt.core.common.MqttPendingQos2Publish; +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.dromara.mica.mqtt.core.server.MqttServerProcessor; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerAuthHandler; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerPublishPermission; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerSubscribeValidator; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerUniqueIdService; +import org.dromara.mica.mqtt.core.server.dispatcher.IMqttMessageDispatcher; +import org.dromara.mica.mqtt.core.server.enums.MessageType; +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.dromara.mica.mqtt.core.server.event.IMqttMessageListener; +import org.dromara.mica.mqtt.core.server.event.IMqttSessionListener; +import org.dromara.mica.mqtt.core.server.model.Message; +import org.dromara.mica.mqtt.core.server.session.IMqttSessionManager; +import org.dromara.mica.mqtt.core.server.store.IMqttMessageStore; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Node; +import org.tio.core.Tio; +import org.tio.core.TioConfig; +import org.tio.utils.hutool.StrUtil; +import org.tio.utils.mica.Pair; +import org.tio.utils.timer.TimerTaskService; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; + +/** + * mqtt broker 处理器 + * + * @author L.cm + */ +public class DefaultMqttServerProcessor implements MqttServerProcessor { + private static final Logger logger = LoggerFactory.getLogger(DefaultMqttServerProcessor.class); + /** + * 2 倍客户端 keepAlive 时间 + */ + private static final long KEEP_ALIVE_UNIT = 2000L; + private final MqttServerCreator serverCreator; + private final long heartbeatTimeout; + private final IMqttMessageStore messageStore; + private final IMqttSessionManager sessionManager; + private final IMqttServerAuthHandler authHandler; + private final IMqttServerUniqueIdService uniqueIdService; + private final IMqttServerSubscribeValidator subscribeValidator; + private final IMqttServerPublishPermission publishPermission; + private final IMqttMessageDispatcher messageDispatcher; + private final IMqttConnectStatusListener connectStatusListener; + private final IMqttSessionListener sessionListener; + private final IMqttMessageListener messageListener; + private final TimerTaskService taskService; + private final ExecutorService executor; + + public DefaultMqttServerProcessor(MqttServerCreator serverCreator, + TimerTaskService taskService, + ExecutorService executor) { + this.serverCreator = serverCreator; + this.heartbeatTimeout = serverCreator.getHeartbeatTimeout() == null ? TioConfig.DEFAULT_HEARTBEAT_TIMEOUT : serverCreator.getHeartbeatTimeout(); + this.messageStore = serverCreator.getMessageStore(); + this.sessionManager = serverCreator.getSessionManager(); + this.authHandler = serverCreator.getAuthHandler(); + this.uniqueIdService = serverCreator.getUniqueIdService(); + this.subscribeValidator = serverCreator.getSubscribeValidator(); + this.publishPermission = serverCreator.getPublishPermission(); + this.messageDispatcher = serverCreator.getMessageDispatcher(); + this.connectStatusListener = serverCreator.getConnectStatusListener(); + this.sessionListener = serverCreator.getSessionListener(); + this.messageListener = serverCreator.getMessageListener(); + this.taskService = taskService; + this.executor = executor; + } + + @Override + public void processConnect(ChannelContext context, MqttConnectMessage mqttMessage) { + MqttConnectPayload payload = mqttMessage.payload(); + // 参数 + String clientId = payload.clientIdentifier(); + String userName = payload.username(); + String password = payload.password(); + // 1. 获取唯一id,用于 mqtt 内部绑定,部分用户的业务采用 userName 作为唯一id,故抽象之,默认:uniqueId == clientId + String uniqueId = uniqueIdService.getUniqueId(context, clientId, userName, password); + // 2. 客户端必须提供 uniqueId, 不管 cleanSession 是否为1, 此处没有参考标准协议实现 + if (StrUtil.isBlank(uniqueId)) { + connAckByReturnCode(clientId, uniqueId, context, MqttConnectReasonCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED); + return; + } + // 3. 认证 + if (authHandler != null && !authHandler.verifyAuthenticate(context, uniqueId, clientId, userName, password)) { + connAckByReturnCode(clientId, uniqueId, context, MqttConnectReasonCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); + return; + } + // 认证成功 + context.setAccepted(true); + // 4. 判断 uniqueId 是否在多个地方使用,如果在其他地方有使用,先解绑 + ChannelContext otherContext = Tio.getByBsId(context.getTioConfig(), uniqueId); + if (otherContext != null) { + Tio.unbindBsId(otherContext); + String remark = String.format("uniqueId:[%s] clientId:[%s] 被踢出,请检查是否有相同 clientId 互踢,新 contextId:[%s]", uniqueId, clientId, context.getId()); + Tio.remove(otherContext, remark); + cleanSession(uniqueId); + } + // 4.5 广播上线消息,避免一个 uniqueId 多个集群服务器中连接。 + sendConnected(context, uniqueId); + // 5. 绑定 uniqueId、保存 username + Tio.bindBsId(context, uniqueId); + if (StrUtil.isNotBlank(userName)) { + Tio.bindUser(context, userName); + } + // 6. 心跳超时时间,当然这个值如果小于全局配置(默认:120s),定时检查的时间间隔还是以全局为准,只是在判断时用此值 + MqttConnectVariableHeader variableHeader = mqttMessage.variableHeader(); + int keepAliveSeconds = variableHeader.keepAliveTimeSeconds(); + // keepAlive * keepAliveBackoff * 2 时间作为服务端心跳超时时间,如果配置同全局默认不设置,节约内存 + long keepAliveTimeout = keepAliveSeconds * KEEP_ALIVE_UNIT; + if (keepAliveSeconds > 0 && heartbeatTimeout != keepAliveTimeout) { + context.setHeartbeatTimeout(keepAliveTimeout); + } + // 7. session 处理,先默认全部连接关闭时清除,mqtt5 为 CleanStart, + // 按照 mqtt 协议的规则是下一次连接时清除,emq 是添加了全局 session 超时,关闭时激活 session 有效期倒计时 +// boolean cleanSession = variableHeader.isCleanSession(); +// if (cleanSession) { +// // TODO L.cm 考虑 session 处理 可参数: https://www.emqx.com/zh/blog/mqtt-session +// // mqtt v5.0 会话超时时间 +// MqttProperties properties = variableHeader.properties(); +// Integer sessionExpiryInterval = properties.getPropertyValue(MqttProperties.MqttPropertyType.SESSION_EXPIRY_INTERVAL); +// System.out.println(sessionExpiryInterval); +// } + // 8. 存储遗嘱消息 + boolean willFlag = variableHeader.isWillFlag(); + if (willFlag) { + Message willMessage = new Message(); + willMessage.setMessageType(MessageType.DOWN_STREAM); + willMessage.setFromClientId(uniqueId); + willMessage.setFromUsername(userName); + willMessage.setTopic(payload.willTopic()); + byte[] willMessageInBytes = payload.willMessageInBytes(); + if (willMessageInBytes != null) { + willMessage.setPayload(willMessageInBytes); + } + willMessage.setQos(variableHeader.willQos()); + willMessage.setRetain(variableHeader.isWillRetain()); + willMessage.setTimestamp(System.currentTimeMillis()); + Node clientNode = context.getClientNode(); + // 客户端 ip:端口 + willMessage.setPeerHost(clientNode.getPeerHost()); + willMessage.setNode(serverCreator.getNodeName()); + messageStore.addWillMessage(uniqueId, willMessage); + } + // 9. 返回 ack + connAckByReturnCode(clientId, uniqueId, context, MqttConnectReasonCode.CONNECTION_ACCEPTED); + // 10. 在线状态 + executor.execute(() -> { + try { + connectStatusListener.online(context, uniqueId, userName); + } catch (Throwable e) { + logger.error("Mqtt server uniqueId:{} clientId:{} online notify error.", uniqueId, clientId, e); + } + }); + } + + private static void connAckByReturnCode(String clientId, String uniqueId, ChannelContext context, MqttConnectReasonCode returnCode) { + MqttConnAckMessage message = MqttConnAckMessage.builder() + .returnCode(returnCode) + .sessionPresent(false) + .build(); + boolean result = Tio.send(context, message); + if (returnCode.isAccepted()) { + logger.info("Connect successful, clientId: {} uniqueId:{} result:{}", clientId, uniqueId, result); + } else { + logger.error("Connect error - clientId: {} uniqueId:{} returnCode:{} result:{}", clientId, uniqueId, returnCode, result); + } + } + + private void sendConnected(ChannelContext context, String uniqueId) { + Message message = new Message(); + message.setClientId(uniqueId); + message.setMessageType(MessageType.CONNECT); + message.setNode(serverCreator.getNodeName()); + message.setTimestamp(System.currentTimeMillis()); + Node clientNode = context.getClientNode(); + message.setPeerHost(clientNode.getPeerHost()); + messageDispatcher.send(message); + } + + private void cleanSession(String clientId) { + try { + sessionManager.remove(clientId); + } catch (Throwable throwable) { + logger.error("Mqtt server clientId:{} session clean error.", clientId, throwable); + } + } + + @Override + public void processPublish(ChannelContext context, MqttPublishMessage message) { + String clientId = context.getBsId(); + MqttFixedHeader fixedHeader = message.fixedHeader(); + MqttQoS mqttQoS = fixedHeader.qosLevel(); + MqttPublishVariableHeader variableHeader = message.variableHeader(); + String topicName = variableHeader.topicName(); + // 1. 权限判断,在 MQTT v3.1 和 v3.1.1 协议中,发布操作被拒绝后服务器无任何报文错误返回,这是协议设计的一个缺陷。但在 MQTT v5.0 协议上已经支持应答一个相应的错误报文。 + if (publishPermission != null && !publishPermission.verifyPermission(context, clientId, topicName, mqttQoS, fixedHeader.isRetain())) { + logger.error("Mqtt clientId:{} username:{} topic:{} 没有发布权限。", clientId, context.getUserId(), topicName); + return; + } + // 2. 处理发布逻辑 + int packetId = variableHeader.packetId(); + logger.debug("Publish - clientId:{} topicName:{} mqttQoS:{} packetId:{}", clientId, topicName, mqttQoS, packetId); + switch (mqttQoS) { + case QOS0: + invokeListenerForPublish(context, clientId, mqttQoS, topicName, message); + break; + case QOS1: + invokeListenerForPublish(context, clientId, mqttQoS, topicName, message); + if (packetId != -1) { + MqttMessage messageAck = MqttPubAckMessage.builder() + .packetId(packetId) + .build(); + boolean resultPubAck = Tio.send(context, messageAck); + logger.debug("Publish - PubAck send clientId:{} topicName:{} mqttQoS:{} packetId:{} result:{}", clientId, topicName, mqttQoS, packetId, resultPubAck); + } + break; + case QOS2: + if (packetId != -1) { + MqttFixedHeader pubRecFixedHeader = new MqttFixedHeader(MqttMessageType.PUBREC, false, MqttQoS.QOS0, false, 0); + MqttMessage pubRecMessage = new MqttMessage(pubRecFixedHeader, MqttMessageIdVariableHeader.from(packetId)); + MqttPendingQos2Publish pendingQos2Publish = new MqttPendingQos2Publish(message, pubRecMessage); + // 添加重试 + sessionManager.addPendingQos2Publish(clientId, packetId, pendingQos2Publish); + pendingQos2Publish.startPubRecRetransmitTimer(taskService, context); + // 发送消息 + boolean resultPubRec = Tio.send(context, pubRecMessage); + logger.debug("Publish - PubRec send clientId:{} topicName:{} mqttQoS:{} packetId:{} result:{}", clientId, topicName, mqttQoS, packetId, resultPubRec); + } + break; + case FAILURE: + default: + break; + } + } + + @Override + public void processPubAck(ChannelContext context, MqttMessageIdVariableHeader variableHeader) { + int packetId = variableHeader.messageId(); + String clientId = context.getBsId(); + logger.debug("PubAck - clientId:{}, packetId:{}", clientId, packetId); + MqttPendingPublish pendingPublish = sessionManager.getPendingPublish(clientId, packetId); + if (pendingPublish == null) { + return; + } + pendingPublish.onPubAckReceived(); + sessionManager.removePendingPublish(clientId, packetId); + } + + @Override + public void processPubRec(ChannelContext context, MqttMessageIdVariableHeader variableHeader) { + String clientId = context.getBsId(); + int packetId = variableHeader.messageId(); + logger.debug("PubRec - clientId:{}, packetId:{}", clientId, packetId); + MqttPendingPublish pendingPublish = sessionManager.getPendingPublish(clientId, packetId); + if (pendingPublish == null) { + return; + } + pendingPublish.onPubAckReceived(); + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, MqttQoS.QOS1, false, 0); + MqttMessage pubRelMessage = new MqttMessage(fixedHeader, variableHeader); + + pendingPublish.setPubRelMessage(pubRelMessage); + pendingPublish.startPubRelRetransmissionTimer(taskService, context); + + boolean result = Tio.send(context, pubRelMessage); + logger.debug("Publish - PubRel send clientId:{} packetId:{} result:{}", clientId, packetId, result); + } + + @Override + public void processPubRel(ChannelContext context, MqttMessageIdVariableHeader variableHeader) { + String clientId = context.getBsId(); + int packetId = variableHeader.messageId(); + logger.debug("PubRel - clientId:{}, packetId:{}", clientId, packetId); + MqttPendingQos2Publish pendingQos2Publish = sessionManager.getPendingQos2Publish(clientId, packetId); + if (pendingQos2Publish != null) { + MqttPublishMessage incomingPublish = pendingQos2Publish.getIncomingPublish(); + String topicName = incomingPublish.variableHeader().topicName(); + MqttFixedHeader incomingFixedHeader = incomingPublish.fixedHeader(); + MqttQoS mqttQoS = incomingFixedHeader.qosLevel(); + invokeListenerForPublish(context, clientId, mqttQoS, topicName, incomingPublish); + pendingQos2Publish.onPubRelReceived(); + sessionManager.removePendingQos2Publish(clientId, packetId); + } + MqttMessage message = MqttMessageFactory.newMessage( + new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.QOS0, false, 0), + MqttMessageIdVariableHeader.from(packetId), null); + + boolean result = Tio.send(context, message); + logger.debug("Publish - PubComp send clientId:{} packetId:{} result:{}", clientId, packetId, result); + } + + @Override + public void processPubComp(ChannelContext context, MqttMessageIdVariableHeader variableHeader) { + int packetId = variableHeader.messageId(); + String clientId = context.getBsId(); + logger.debug("PubComp - clientId:{}, packetId:{}", clientId, packetId); + MqttPendingPublish pendingPublish = sessionManager.getPendingPublish(clientId, packetId); + if (pendingPublish != null) { + pendingPublish.onPubCompReceived(); + sessionManager.removePendingPublish(clientId, packetId); + } + } + + @Override + public void processSubscribe(ChannelContext context, MqttSubscribeMessage message) { + String clientId = context.getBsId(); + int packetId = message.variableHeader().messageId(); + // 1. 校验订阅的 topicFilter + List topicSubscriptionList = message.payload().topicSubscriptions(); + List grantedQosList = new ArrayList<>(); + // 校验订阅 + List subscribedTopicList = new ArrayList<>(); + boolean enableSubscribeValidator = subscribeValidator != null; + for (MqttTopicSubscription subscription : topicSubscriptionList) { + String topicFilter = subscription.topicFilter(); + // 校验 topicFilter 是否合法 + TopicUtil.validateTopicFilter(topicFilter); + MqttQoS mqttQoS = subscription.qualityOfService(); + // 校验是否可以订阅 + if (enableSubscribeValidator && !subscribeValidator.verifyTopicFilter(context, clientId, topicFilter, mqttQoS)) { + grantedQosList.add(MqttQoS.FAILURE); + logger.error("Subscribe - clientId:{} username:{} topicFilter:{} mqttQoS:{} 没有订阅权限 packetId:{}", clientId, context.getUserId(), topicFilter, mqttQoS, packetId); + } else { + grantedQosList.add(mqttQoS); + subscribedTopicList.add(topicFilter); + sessionManager.addSubscribe(topicFilter, clientId, mqttQoS.value()); + logger.info("Subscribe - clientId:{} topicFilter:{} mqttQoS:{} packetId:{}", clientId, topicFilter, mqttQoS, packetId); + publishSubscribedEvent(context, clientId, topicFilter, mqttQoS); + } + } + // 3. 返回 ack + MqttMessage subAckMessage = MqttSubAckMessage.builder() + .addGrantedQosList(grantedQosList) + .packetId(packetId) + .build(); + boolean result = Tio.send(context, subAckMessage); + logger.info("Subscribe - SubAck send clientId:{} subscribedTopicList:{} packetId:{} result:{}", clientId, subscribedTopicList, packetId, result); + // 4. 发送保留消息 + for (String topic : subscribedTopicList) { + executor.submit(() -> { + List retainMessageList = messageStore.getRetainMessage(topic); + if (retainMessageList != null && !retainMessageList.isEmpty()) { + for (Message retainMessage : retainMessageList) { + messageDispatcher.sendRetainMessage(context, clientId, retainMessage); + } + } + }); + } + } + + /** + * 发送订阅事件 + * + * @param context ChannelContext + * @param clientId clientId + * @param topicFilter topicFilter + * @param mqttQoS MqttQoS + */ + private void publishSubscribedEvent(ChannelContext context, String clientId, String topicFilter, MqttQoS mqttQoS) { + if (sessionListener == null) { + return; + } + executor.execute(() -> { + try { + sessionListener.onSubscribed(context, clientId, topicFilter, mqttQoS); + } catch (Throwable e) { + logger.error("Mqtt server clientId:{} topicFilter:{} onUnsubscribed error.", clientId, topicFilter, e); + } + }); + } + + @Override + public void processUnSubscribe(ChannelContext context, MqttUnSubscribeMessage message) { + String clientId = context.getBsId(); + int packetId = message.variableHeader().messageId(); + List topicFilterList = message.payload().topics(); + for (String topicFilter : topicFilterList) { + sessionManager.removeSubscribe(topicFilter, clientId); + publishUnsubscribedEvent(context, clientId, topicFilter); + } + logger.info("UnSubscribe - clientId:{} Topic:{} packetId:{}", clientId, topicFilterList, packetId); + MqttMessage unSubMessage = MqttUnSubAckMessage.builder() + .packetId(packetId) + .build(); + boolean result = Tio.send(context, unSubMessage); + logger.debug("UnSubscribe - UnSubAck send clientId:{} result:{}", clientId, result); + } + + /** + * 发送取消订阅事件 + * + * @param context ChannelContext + * @param clientId clientId + * @param topicFilter topicFilter + */ + private void publishUnsubscribedEvent(ChannelContext context, String clientId, String topicFilter) { + if (sessionListener == null) { + return; + } + executor.execute(() -> { + try { + sessionListener.onUnsubscribed(context, clientId, topicFilter); + } catch (Throwable e) { + logger.error("Mqtt server clientId:{} topicFilter:{} onUnsubscribed error.", clientId, topicFilter, e); + } + }); + } + + @Override + public void processPingReq(ChannelContext context) { + String clientId = context.getBsId(); + boolean result = Tio.send(context, MqttMessage.PINGRESP); + logger.debug("PingReq - PingResp send clientId:{} result:{}", clientId, result); + } + + @Override + public void processDisConnect(ChannelContext context) { + String clientId = context.getBsId(); + logger.info("DisConnect - clientId:{} contextId:{}", clientId, context.getId()); + // 设置正常断开的标识 + context.setBizStatus(true); + Tio.remove(context, "Mqtt DisConnect"); + } + + /** + * 处理订阅的消息 + * + * @param context ChannelContext + * @param clientId clientId + * @param topicName topicName + * @param publishMessage MqttPublishMessage + */ + private void invokeListenerForPublish(ChannelContext context, String clientId, MqttQoS mqttQoS, + String topicName, MqttPublishMessage publishMessage) { + MqttFixedHeader fixedHeader = publishMessage.fixedHeader(); + Node clientNode = context.getClientNode(); + boolean isRetain = fixedHeader.isRetain(); + byte[] payload = publishMessage.payload(); + // 1. retain 消息逻辑 + if (isRetain) { + Pair retainPair = TopicUtil.retainTopicName(topicName); + int timeOut = retainPair.getRight(); + if (timeOut < 0) { + logger.error("MqttPublishMessage topic {} 不符合 $retain/${ttl}/topic 规则.", topicName); + return; + } + topicName = retainPair.getLeft(); + // qos == 0 or payload is none,then clear previous retain message + if (MqttQoS.QOS0 == mqttQoS || payload == null || payload.length == 0) { + this.messageStore.clearRetainMessage(topicName); + } else { + Message retainMessage = new Message(); + retainMessage.setTopic(topicName); + retainMessage.setQos(mqttQoS.value()); + retainMessage.setPayload(payload); + retainMessage.setFromClientId(clientId); + retainMessage.setMessageType(MessageType.DOWN_STREAM); + retainMessage.setRetain(true); + retainMessage.setDup(fixedHeader.isDup()); + retainMessage.setTimestamp(System.currentTimeMillis()); + // 客户端 ip:端口 + retainMessage.setPeerHost(clientNode.getPeerHost()); + retainMessage.setNode(serverCreator.getNodeName()); + this.messageStore.addRetainMessage(topicName, timeOut, retainMessage); + } + } + // topic + final String topic = topicName; + // 2. 消息监听 + if (messageListener != null) { + executor.submit(() -> { + try { + messageListener.onMessage(context, clientId, topic, mqttQoS, publishMessage); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + }); + } + // 2. message + MqttPublishVariableHeader variableHeader = publishMessage.variableHeader(); + Message message = new Message(); + message.setId(variableHeader.packetId()); + // 注意:broker 消息转发是不需要设置 toClientId 而是应该按 topic 找到订阅的客户端进行发送 + message.setFromClientId(clientId); + message.setTopic(topic); + message.setQos(mqttQoS.value()); + if (payload != null) { + message.setPayload(payload); + } + message.setMessageType(MessageType.UP_STREAM); + // 已订阅状态下的监听,此时消息被视为“实时发布”而非“保留触发”,标志位不会被激活 + message.setRetain(false); + message.setDup(fixedHeader.isDup()); + message.setTimestamp(System.currentTimeMillis()); + // 客户端 ip:端口 + message.setPeerHost(clientNode.getPeerHost()); + message.setNode(serverCreator.getNodeName()); + // 4. 消息流转 + executor.submit(() -> { + try { + messageDispatcher.send(message); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + }); + } + +} diff --git a/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerUniqueIdServiceImpl.java b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerUniqueIdServiceImpl.java new file mode 100644 index 0000000..f2423f1 --- /dev/null +++ b/mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerUniqueIdServiceImpl.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.core.server.support; + +import org.dromara.mica.mqtt.core.server.auth.IMqttServerUniqueIdService; +import org.tio.core.ChannelContext; + +/** + * 默认的 mqtt 服务端唯一 id 绑定,使用 clientId + * + * @author L.cm + */ +public class DefaultMqttServerUniqueIdServiceImpl implements IMqttServerUniqueIdService { + + @Override + public String getUniqueId(ChannelContext context, String clientId, String userName, String password) { + return clientId; + } + +} diff --git a/mica-mqtt-server/src/main/moditect/module-info.java b/mica-mqtt-server/src/main/moditect/module-info.java new file mode 100644 index 0000000..c7c4f55 --- /dev/null +++ b/mica-mqtt-server/src/main/moditect/module-info.java @@ -0,0 +1,22 @@ +open module org.dromara.mica.mqtt.server { + requires transitive org.dromara.mica.mqtt.common; + requires transitive net.dreamlu.mica.net.http; + requires java.management; + + exports org.dromara.mica.mqtt.core.server; + exports org.dromara.mica.mqtt.core.server.auth; + exports org.dromara.mica.mqtt.core.server.broker; + exports org.dromara.mica.mqtt.core.server.cluster; + exports org.dromara.mica.mqtt.core.server.dispatcher; + exports org.dromara.mica.mqtt.core.server.enums; + exports org.dromara.mica.mqtt.core.server.event; + exports org.dromara.mica.mqtt.core.server.http.handler; + exports org.dromara.mica.mqtt.core.server.interceptor; + exports org.dromara.mica.mqtt.core.server.listener; + exports org.dromara.mica.mqtt.core.server.model; + exports org.dromara.mica.mqtt.core.server.protocol; + exports org.dromara.mica.mqtt.core.server.serializer; + exports org.dromara.mica.mqtt.core.server.session; + exports org.dromara.mica.mqtt.core.server.store; + exports org.dromara.mica.mqtt.core.server.support; +} diff --git a/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/MqttSessionManagerTest.java b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/MqttSessionManagerTest.java new file mode 100644 index 0000000..cc636ca --- /dev/null +++ b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/MqttSessionManagerTest.java @@ -0,0 +1,90 @@ +package org.dromara.mica.mqtt.core.server.test; + +import org.dromara.mica.mqtt.core.server.session.IMqttSessionManager; +import org.dromara.mica.mqtt.core.server.session.InMemoryMqttSessionManager; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +/** + * MqttSessionManager 测试 + * + * @author L.cm + */ +class MqttSessionManagerTest { + + @Test + void testAdd() { + IMqttSessionManager topicManager = new InMemoryMqttSessionManager(); + topicManager.addSubscribe("/sys/1/456/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/2/456/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/3/4567/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/4/45678/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/1/4561/thing/model/down_raw", "client1", 0); + topicManager.addSubscribe("/sys/2/45612/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/+/+/thing/model/down_raw", "client1", 0); + topicManager.addSubscribe("/sys/3/456/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/12/456/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/11/4567/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/111/45678/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/123/4561/thing/model/down_raw", "client1", 0); + topicManager.addSubscribe("/sys/123/45612/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/1/+/thing/model/down_raw", "client1", 0); + topicManager.addSubscribe("/sys/1/456/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/2/456/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/3/4567/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/4/45678/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/1/4561/thing/model/down_raw", "client2", 0); + topicManager.addSubscribe("/sys/2/45612/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/+/+/thing/model/down_raw", "client2", 0); + topicManager.addSubscribe("/sys/3/456/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/12/456/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/11/4567/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/111/45678/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/123/4561/thing/model/down_raw", "client2", 0); + topicManager.addSubscribe("/sys/123/45612/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/1/+/thing/model/down_raw", "client2", 0); + topicManager.addSubscribe("/sys/1/456/thing/model/down_raw", "client3", 1); + topicManager.addSubscribe("/sys/2/456/thing/model/down_raw", "client3", 1); + topicManager.addSubscribe("/sys/3/4567/thing/model/down_raw", "client3", 1); + topicManager.addSubscribe("/sys/4/45678/thing/model/down_raw", "client3", 1); + topicManager.addSubscribe("/sys/1/4561/thing/model/down_raw", "client3", 0); + topicManager.addSubscribe("/sys/2/45612/thing/model/down_raw", "client3", 1); + topicManager.addSubscribe("/sys/+/+/thing/model/down_raw", "client3", 0); + topicManager.addSubscribe("/sys/3/456/thing/model/down_raw", "client3", 1); + topicManager.addSubscribe("/sys/12/456/thing/model/down_raw", "client3", 1); + topicManager.addSubscribe("/sys/11/4567/thing/model/down_raw", "client3", 1); + topicManager.addSubscribe("/sys/111/45678/thing/model/down_raw", "client3", 1); + topicManager.addSubscribe("/sys/123/4561/thing/model/down_raw", "client3", 0); + topicManager.addSubscribe("/sys/123/45612/thing/model/down_raw", "client3", 1); + topicManager.addSubscribe("/sys/1/+/thing/model/down_raw", "client3", 0); + topicManager.addSubscribe("$share/group1/sys/123/456/thing/model/down_raw", "client1", 0); + topicManager.addSubscribe("$queue/sys/123/456/thing/model/down_raw", "client31", 0); + topicManager.addSubscribe("$share/group1/sys/123/456/thing/model/down_raw", "client2", 0); + topicManager.addSubscribe("$queue/sys/123/456/thing/model/down_raw", "client2", 0); + topicManager.addSubscribe("$share/group1/sys/123/456/thing/model/down_raw", "client3", 0); + List subscribeList = topicManager.getSubscriptions("client3"); + Assertions.assertFalse(subscribeList.isEmpty()); + } + + @Test + void testRemove() { + IMqttSessionManager topicManager = new InMemoryMqttSessionManager(); + topicManager.removeSubscribe("/sys/1/456/thing/model/down_raw", "client1"); + topicManager.removeSubscribe("$share/group1/sys/123/456/thing/model/down_raw", "client1"); + topicManager.removeSubscribe("$share/group1/sys/123/456/thing/model/down_raw", "client1"); + List subscribeList = topicManager.getSubscriptions("client3"); + Assertions.assertTrue(subscribeList.isEmpty()); + } + + @Test + void testPacketId() { + IMqttSessionManager topicManager = new InMemoryMqttSessionManager(); + for (int i = 0; i < Short.MAX_VALUE * 3; i++) { + int packetId = topicManager.getPacketId("client1"); + System.out.println(packetId); + } + } + +} diff --git a/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerDeepAnalysis.java b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerDeepAnalysis.java new file mode 100644 index 0000000..5e41c52 --- /dev/null +++ b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerDeepAnalysis.java @@ -0,0 +1,288 @@ +package org.dromara.mica.mqtt.core.server.test; + +import org.dromara.mica.mqtt.core.server.session.TrieTopicManager; +import org.openjdk.jol.info.ClassLayout; +import org.openjdk.jol.info.GraphLayout; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * TrieTopicManager 深度内存分析工具 + * 使用反射分析内部结构的内存占用 + * + * @author AI Assistant + */ +public class TrieTopicManagerDeepAnalysis { + + /** + * 深度分析 TrieTopicManager 的内存占用 + */ + public static void deepAnalyze(TrieTopicManager topicManager) { + System.out.println("=== TrieTopicManager 深度内存分析 ==="); + + try { + // 分析 root 节点 + analyzeRootNode(topicManager); + + // 分析 share 分组 + analyzeShareGroups(topicManager); + + // 分析 queue 分组 + analyzeQueueGroup(topicManager); + + // 分析整体内存分布 + analyzeOverallMemory(topicManager); + + } catch (Exception e) { + System.err.println("深度分析失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 分析 root 节点 + */ + private static void analyzeRootNode(TrieTopicManager topicManager) throws Exception { + System.out.println("\n--- Root 节点分析 ---"); + + Field rootField = TrieTopicManager.class.getDeclaredField("root"); + rootField.setAccessible(true); + Object rootNode = rootField.get(topicManager); + + if (rootNode != null) { + System.out.println("Root 节点类型: " + rootNode.getClass().getName()); + System.out.println("Root 节点内存占用: " + + GraphLayout.parseInstance(rootNode).totalSize() + " bytes"); + + // 分析 root 节点的子节点 + analyzeNodeChildren(rootNode, "root"); + } + } + + /** + * 分析 share 分组 + */ + private static void analyzeShareGroups(TrieTopicManager topicManager) throws Exception { + System.out.println("\n--- Share 分组分析 ---"); + + Field shareField = TrieTopicManager.class.getDeclaredField("share"); + shareField.setAccessible(true); + Object shareMap = shareField.get(topicManager); + + if (shareMap instanceof ConcurrentHashMap) { + ConcurrentHashMap shareGroups = (ConcurrentHashMap) shareMap; + System.out.println("Share 分组数量: " + shareGroups.size()); + + long totalShareSize = 0; + int totalChildren = 0; + + for (Map.Entry entry : shareGroups.entrySet()) { + String groupName = entry.getKey().toString(); + Object groupNode = entry.getValue(); + + long groupSize = GraphLayout.parseInstance(groupNode).totalSize(); + totalShareSize += groupSize; + + System.out.println(" 分组 '" + groupName + "': " + groupSize + " bytes"); + + // 分析每个分组的子节点 + int childrenCount = analyzeNodeChildren(groupNode, "share." + groupName); + totalChildren += childrenCount; + } + + System.out.println("Share 分组总内存占用: " + totalShareSize + " bytes"); + System.out.println("Share 分组总子节点数: " + totalChildren); + } + } + + /** + * 分析 queue 分组 + */ + private static void analyzeQueueGroup(TrieTopicManager topicManager) throws Exception { + System.out.println("\n--- Queue 分组分析 ---"); + + Field queueField = TrieTopicManager.class.getDeclaredField("queue"); + queueField.setAccessible(true); + Object queueNode = queueField.get(topicManager); + + if (queueNode != null) { + System.out.println("Queue 节点类型: " + queueNode.getClass().getName()); + System.out.println("Queue 节点内存占用: " + + GraphLayout.parseInstance(queueNode).totalSize() + " bytes"); + + // 分析 queue 节点的子节点 + analyzeNodeChildren(queueNode, "queue"); + } + } + + /** + * 分析节点的子节点 + */ + private static int analyzeNodeChildren(Object node, String nodePath) throws Exception { + Field childrenField = node.getClass().getDeclaredField("children"); + childrenField.setAccessible(true); + Object childrenMap = childrenField.get(node); + + if (childrenMap instanceof Map) { + Map children = (Map) childrenMap; + int childrenCount = children.size(); + + if (childrenCount > 0) { + System.out.println(" " + nodePath + " 子节点数: " + childrenCount); + + // 只显示前几个子节点的详细信息,避免输出过多 + int displayCount = Math.min(childrenCount, 5); + int index = 0; + + for (Map.Entry entry : children.entrySet()) { + if (index >= displayCount) { + if (childrenCount > displayCount) { + System.out.println(" ... 还有 " + (childrenCount - displayCount) + " 个子节点"); + } + break; + } + + String childKey = entry.getKey().toString(); + Object childNode = entry.getValue(); + + long childSize = GraphLayout.parseInstance(childNode).totalSize(); + System.out.println(" " + nodePath + "." + childKey + ": " + childSize + " bytes"); + + index++; + } + } + + return childrenCount; + } + + return 0; + } + + /** + * 分析整体内存分布 + */ + private static void analyzeOverallMemory(TrieTopicManager topicManager) { + System.out.println("\n--- 整体内存分析 ---"); + + GraphLayout layout = GraphLayout.parseInstance(topicManager); + + System.out.println("总内存占用: " + layout.totalSize() + " bytes"); + System.out.println("对象数量: " + layout.totalCount()); + + System.out.println("\n内存分布详情:"); + System.out.println(layout.toFootprint()); + } + + /** + * 分析内存增长模式 + */ + public static void analyzeMemoryGrowthPattern() { + System.out.println("=== 内存增长模式分析 ==="); + + TrieTopicManager topicManager = new TrieTopicManager(); + long baseSize = GraphLayout.parseInstance(topicManager).totalSize(); + + System.out.println("初始内存占用: " + baseSize + " bytes"); + System.out.println("订阅数量\t内存增长\t平均每订阅内存\t内存效率"); + + for (int batchSize = 100; batchSize <= 5000; batchSize += 100) { + // 添加一批订阅 + for (int i = 0; i < batchSize; i++) { + topicManager.addSubscribe("/test/" + i + "/topic", "client" + i, i % 3); + } + + long currentSize = GraphLayout.parseInstance(topicManager).totalSize(); + long memoryIncrease = currentSize - baseSize; + double avgMemoryPerSubscription = (double) memoryIncrease / batchSize; + double memoryEfficiency = (double) batchSize / memoryIncrease * 1000; // 每KB内存支持的订阅数 + + System.out.printf("%d\t\t%d bytes\t%.2f bytes\t%.2f 订阅/KB%n", + batchSize, memoryIncrease, avgMemoryPerSubscription, memoryEfficiency); + } + } + + /** + * 分析不同 topic 模式的内存占用 + */ + public static void analyzeTopicPatternMemory() { + System.out.println("=== 不同 Topic 模式内存分析 ==="); + + String[] patterns = { + "/simple/topic", // 简单路径 + "/sys/+/+/thing/model/down_raw", // 通配符路径 + "$share/group1/sys/123/456/topic", // 共享订阅 + "$queue/sys/123/456/topic", // 队列订阅 + "/very/long/topic/path/with/many/levels/and/segments" // 长路径 + }; + + for (String pattern : patterns) { + TrieTopicManager topicManager = new TrieTopicManager(); + + // 添加多个客户端订阅相同模式 + for (int i = 0; i < 100; i++) { + topicManager.addSubscribe(pattern, "client" + i, i % 3); + } + + long memorySize = GraphLayout.parseInstance(topicManager).totalSize(); + System.out.printf("模式: %-50s | 内存: %s | 平均每订阅: %.2f bytes%n", + pattern, + formatBytes(memorySize), + (double) memorySize / 100); + } + } + + /** + * 格式化字节数 + */ + private static String formatBytes(long bytes) { + if (bytes < 1024) { + return bytes + " B"; + } else if (bytes < 1024 * 1024) { + return String.format("%.2f KB", bytes / 1024.0); + } else { + return String.format("%.2f MB", bytes / (1024.0 * 1024.0)); + } + } + + /** + * 主方法,运行所有分析 + */ + public static void main(String[] args) { + // 创建测试数据 + TrieTopicManager topicManager = createTestData(); + + // 运行深度分析 + deepAnalyze(topicManager); + + System.out.println("\n" + "================================================================================\n"); + + // 分析内存增长模式 + analyzeMemoryGrowthPattern(); + + System.out.println("\n" + "================================================================================\n"); + + // 分析不同 topic 模式 + analyzeTopicPatternMemory(); + } + + /** + * 创建测试数据 + */ + private static TrieTopicManager createTestData() { + TrieTopicManager topicManager = new TrieTopicManager(); + + // 添加各种类型的订阅 + topicManager.addSubscribe("/sys/1/456/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/+/+/thing/model/down_raw", "client1", 0); + topicManager.addSubscribe("$share/group1/sys/123/456/thing/model/down_raw", "client1", 0); + topicManager.addSubscribe("$queue/sys/123/456/thing/model/down_raw", "client1", 1); + + topicManager.addSubscribe("/sys/2/456/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("/sys/+/+/thing/model/down_raw", "client2", 0); + topicManager.addSubscribe("$share/group1/sys/123/456/thing/model/down_raw", "client2", 1); + + return topicManager; + } +} diff --git a/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerMemoryAnalysisTest.java b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerMemoryAnalysisTest.java new file mode 100644 index 0000000..d0f25b4 --- /dev/null +++ b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerMemoryAnalysisTest.java @@ -0,0 +1,215 @@ +package org.dromara.mica.mqtt.core.server.test; + +import org.dromara.mica.mqtt.core.server.session.TrieTopicManager; +import org.openjdk.jol.info.ClassLayout; +import org.openjdk.jol.info.GraphLayout; +import org.openjdk.jol.vm.VM; + +import java.util.concurrent.TimeUnit; + +/** + * TrieTopicManager 内存占用分析测试 + * 使用 jol-core 进行详细的内存分析 + * + * @author AI Assistant + */ +public class TrieTopicManagerMemoryAnalysisTest { + + public static void main(String[] args) { + // 1. 显示 JVM 信息 + System.out.println("=== JVM 信息 ==="); + System.out.println(VM.current().details()); + System.out.println(); + + // 2. 分析空 TrieTopicManager 的内存占用 + System.out.println("=== 空 TrieTopicManager 内存分析 ==="); + analyzeEmptyTrieTopicManager(); + System.out.println(); + + // 3. 分析添加订阅后的内存占用 + System.out.println("=== 添加订阅后内存分析 ==="); + analyzeTrieTopicManagerWithSubscriptions(); + System.out.println(); + + // 4. 分析大量订阅的内存占用 + System.out.println("=== 大量订阅内存分析 ==="); + analyzeTrieTopicManagerWithManySubscriptions(); + System.out.println(); + + // 5. 分析共享订阅的内存占用 + System.out.println("=== 共享订阅内存分析 ==="); + analyzeTrieTopicManagerWithShareSubscriptions(); + System.out.println(); + + // 6. 内存增长趋势分析 + System.out.println("=== 内存增长趋势分析 ==="); + analyzeMemoryGrowthTrend(); + + // 7. 性能与内存对比测试 + System.out.println(); + performanceVsMemoryTest(); + } + + /** + * 分析空 TrieTopicManager 的内存占用 + */ + private static void analyzeEmptyTrieTopicManager() { + TrieTopicManager topicManager = new TrieTopicManager(); + + // 分析对象头信息 + System.out.println("空 TrieTopicManager 对象布局:"); + System.out.println(ClassLayout.parseInstance(topicManager).toPrintable()); + + // 分析整个对象图 + System.out.println("空 TrieTopicManager 对象图:"); + System.out.println(GraphLayout.parseInstance(topicManager).toPrintable()); + + // 获取总内存占用 + long totalSize = GraphLayout.parseInstance(topicManager).totalSize(); + System.out.println("空 TrieTopicManager 总内存占用: " + totalSize + " bytes (" + + formatBytes(totalSize) + ")"); + } + + /** + * 分析添加订阅后的内存占用 + */ + private static void analyzeTrieTopicManagerWithSubscriptions() { + TrieTopicManager topicManager = new TrieTopicManager(); + + // 添加一些订阅 + topicManager.addSubscribe("/sys/1/456/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/2/456/thing/model/down_raw", "client1", 1); + topicManager.addSubscribe("/sys/+/+/thing/model/down_raw", "client1", 0); + topicManager.addSubscribe("/sys/1/456/thing/model/down_raw", "client2", 1); + + // 分析对象图 + System.out.println("添加订阅后的对象图:"); + System.out.println(GraphLayout.parseInstance(topicManager).toPrintable()); + + // 获取总内存占用 + long totalSize = GraphLayout.parseInstance(topicManager).totalSize(); + System.out.println("添加订阅后总内存占用: " + totalSize + " bytes (" + + formatBytes(totalSize) + ")"); + } + + /** + * 分析大量订阅的内存占用 + */ + private static void analyzeTrieTopicManagerWithManySubscriptions() { + TrieTopicManager topicManager = new TrieTopicManager(); + + // 添加大量订阅 + for (int i = 0; i < 1000; i++) { + for (int j = 0; j < 10; j++) { + topicManager.addSubscribe("/sys/" + i + "/" + j + "/thing/model/down_raw", + "client" + i, j % 3); + } + } + + // 获取总内存占用 + long totalSize = GraphLayout.parseInstance(topicManager).totalSize(); + System.out.println("大量订阅后总内存占用: " + totalSize + " bytes (" + + formatBytes(totalSize) + ")"); + + // 分析内存分布 + System.out.println("内存分布详情:"); + System.out.println(GraphLayout.parseInstance(topicManager).toFootprint()); + } + + /** + * 分析共享订阅的内存占用 + */ + private static void analyzeTrieTopicManagerWithShareSubscriptions() { + TrieTopicManager topicManager = new TrieTopicManager(); + + // 添加共享订阅 + topicManager.addSubscribe("$share/group1/sys/123/456/thing/model/down_raw", "client1", 0); + topicManager.addSubscribe("$share/group1/sys/123/456/thing/model/down_raw", "client2", 1); + topicManager.addSubscribe("$share/group2/sys/123/456/thing/model/down_raw", "client3", 0); + topicManager.addSubscribe("$queue/sys/123/456/thing/model/down_raw", "client4", 1); + + // 获取总内存占用 + long totalSize = GraphLayout.parseInstance(topicManager).totalSize(); + System.out.println("共享订阅后总内存占用: " + totalSize + " bytes (" + + formatBytes(totalSize) + ")"); + + // 分析共享订阅的内存占用 + System.out.println("共享订阅内存分布:"); + System.out.println(GraphLayout.parseInstance(topicManager).toFootprint()); + } + + /** + * 分析内存增长趋势 + */ + private static void analyzeMemoryGrowthTrend() { + System.out.println("内存增长趋势分析:"); + System.out.println("订阅数量\t内存占用(bytes)\t内存占用(KB)\t内存占用(MB)"); + + TrieTopicManager topicManager = new TrieTopicManager(); + long baseSize = GraphLayout.parseInstance(topicManager).totalSize(); + + for (int subscriptionCount = 0; subscriptionCount <= 10000; subscriptionCount += 1000) { + if (subscriptionCount > 0) { + // 添加订阅 + for (int i = 0; i < 1000; i++) { + topicManager.addSubscribe("/sys/" + subscriptionCount + "/" + i + "/thing/model/down_raw", + "client" + subscriptionCount, i % 3); + } + } + + long currentSize = GraphLayout.parseInstance(topicManager).totalSize(); + long memoryIncrease = currentSize - baseSize; + + System.out.printf("%d\t\t%d\t\t%.2f\t\t%.2f%n", + subscriptionCount, + memoryIncrease, + memoryIncrease / 1024.0, + memoryIncrease / (1024.0 * 1024.0)); + } + } + + /** + * 格式化字节数 + */ + private static String formatBytes(long bytes) { + if (bytes < 1024) { + return bytes + " B"; + } else if (bytes < 1024 * 1024) { + return String.format("%.2f KB", bytes / 1024.0); + } else { + return String.format("%.2f MB", bytes / (1024.0 * 1024.0)); + } + } + + /** + * 性能与内存对比测试 + */ + public static void performanceVsMemoryTest() { + System.out.println("=== 性能与内存对比测试 ==="); + + // 测试不同订阅数量下的性能和内存 + for (int subscriptionCount = 10000; subscriptionCount <= 100000; subscriptionCount += 10000) { + TrieTopicManager topicManager = new TrieTopicManager(); + + // 添加订阅 + long startTime = System.nanoTime(); + for (int i = 0; i < subscriptionCount; i++) { + topicManager.addSubscribe("/sys/" + i + "/thing/model/down_raw", "client" + i, i % 3); + } + long addTime = TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startTime); + + // 测试查找性能 + startTime = System.nanoTime(); + for (int i = 0; i < 10000; i++) { + topicManager.searchSubscribe("/sys/500/thing/model/down_raw"); + } + long searchTime = TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startTime); + + // 获取内存占用 + long memorySize = GraphLayout.parseInstance(topicManager).totalSize(); + + System.out.printf("订阅数: %d, 添加耗时: %d μs, 查找耗时: %d μs, 内存: %s%n", + subscriptionCount, addTime, searchTime, formatBytes(memorySize)); + } + } +} diff --git a/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerTest.java b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerTest.java new file mode 100644 index 0000000..fe5f08f --- /dev/null +++ b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerTest.java @@ -0,0 +1,178 @@ +package org.dromara.mica.mqtt.core.server.test; + +import org.dromara.mica.mqtt.core.server.model.Subscribe; +import org.dromara.mica.mqtt.core.server.session.TrieTopicManager; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * TrieTopicManager 测试 + * + * @author L.cm + */ +class TrieTopicManagerTest { + + /** + * 方便测试 + * + * @param topicFilter topicFilter + * @param topicName topicName + * @return 是否匹配 + */ + private static boolean match(String topicFilter, String topicName) { + TrieTopicManager topicManager = new TrieTopicManager(); + return match(topicManager, topicFilter, topicName); + } + + /** + * 方便测试 + * + * @param topicFilter topicFilter + * @param topicName topicName + * @return 是否匹配 + */ + private static boolean match(TrieTopicManager topicManager, String topicFilter, String topicName) { + String clientId = "client1"; + int qos = 0; + topicManager.addSubscribe(topicFilter, clientId, qos); + List subscribeList = topicManager.searchSubscribe(topicName); + return subscribeList.stream().anyMatch(subscribe -> { + return subscribe.getClientId().equals(clientId) && subscribe.getMqttQoS() == qos; + }); + } + + @Test + void testGetSubscriptions() { + TrieTopicManager topicManager = new TrieTopicManager(); + Assertions.assertTrue(match(topicManager, "123", "123")); + Assertions.assertTrue(match(topicManager, "/1234/", "/1234/")); + Assertions.assertTrue(match(topicManager, "$queue/12345", "12345")); + Assertions.assertTrue(match(topicManager, "$queue//123456", "/123456")); + Assertions.assertTrue(match(topicManager, "$share/test/1234567", "1234567")); + Assertions.assertTrue(match(topicManager, "$share/test//12345678/11/", "/12345678/11/")); + List subscriptions = topicManager.getSubscriptions("client1").stream() + .map(Subscribe::getTopicFilter) + .collect(Collectors.toList()); + Assertions.assertTrue(subscriptions.contains("123")); + Assertions.assertTrue(subscriptions.contains("/1234/")); + Assertions.assertTrue(subscriptions.contains("$queue/12345")); + Assertions.assertTrue(subscriptions.contains("$queue//123456")); + Assertions.assertTrue(subscriptions.contains("$share/test/1234567")); + Assertions.assertTrue(subscriptions.contains("$share/test//12345678/11/")); + } + + @Test + void testMatch() { + // gitee issues #I56BTC /iot/test/# 无法匹配到 /iot/test 和 /iot/test/ + Assertions.assertFalse(match("+", "/iot/test")); + Assertions.assertFalse(match("+", "iot/test")); + Assertions.assertFalse(match("+", "/iot/test")); + Assertions.assertFalse(match("+", "/iot")); + Assertions.assertFalse(match("+/test", "/iot/test")); + Assertions.assertFalse(match("/iot/test/+/", "/iot/test/123")); + + Assertions.assertTrue(match("/iot/test/+", "/iot/test/123")); + Assertions.assertFalse(match("/iot/test/+", "/iot/test/123/")); + Assertions.assertTrue(match("/iot/+/test", "/iot/abc/test")); + Assertions.assertFalse(match("/iot/+/test", "/iot/abc/test/")); + Assertions.assertFalse(match("/iot/+/test", "/iot/abc/test1")); + Assertions.assertTrue(match("/iot/+/+/test", "/iot/abc/123/test")); + Assertions.assertFalse(match("/iot/+/+/test", "/iot/abc/123/test1")); + Assertions.assertFalse(match("/iot/+/+/test", "/iot/abc/123/test/")); + Assertions.assertTrue(match("/iot/+/+/+", "/iot/abc/123/test")); + Assertions.assertFalse(match("/iot/+/+/+", "/iot/abc/123/test/")); + Assertions.assertTrue(match("/iot/+/test", "/iot/a/test")); + Assertions.assertTrue(match("/iot/+/test", "/iot/a/test")); + Assertions.assertFalse(match("/iot/+/+/+", "/iot/a//test/")); + Assertions.assertFalse(match("/iot/+/+/+", "/iot/a/b/c/")); + Assertions.assertFalse(match("/iot/+/+/+", "/iot/a")); + Assertions.assertFalse(TopicUtil.match("/iot/test/+", "/iot/test")); + + Assertions.assertTrue(match("#", "/iot/test")); + Assertions.assertTrue(match("/iot/test/#", "/iot/test")); + Assertions.assertTrue(match("/iot/test/#", "/iot/test/")); + Assertions.assertTrue(match("/iot/test/#", "/iot/test/1")); + Assertions.assertTrue(match("/iot/test/#", "/iot/test/123123/12312")); + + Assertions.assertTrue(match("/iot/test/123", "/iot/test/123")); + } + + @Test + void testMatchGroup() { + Assertions.assertTrue(match("$queue/123", "123")); + Assertions.assertFalse(match("$queue/123", "/123")); + Assertions.assertFalse(match("$queue//123", "123")); + Assertions.assertTrue(match("$queue//123", "/123")); + + Assertions.assertTrue(match("$share/test/123", "123")); + Assertions.assertFalse(match("$share/test/123", "/123")); + Assertions.assertFalse(match("$share/test//123", "123")); + Assertions.assertTrue(match("$share/test//123", "/123")); + } + + @Test + void testAdd() { + TrieTopicManager topicManager = new TrieTopicManager(); + topicManager.addSubscribe("test/+", "client1", 0); + topicManager.addSubscribe("test/123", "client1", 1); + topicManager.addSubscribe("$queue/test/123", "client1", 2); + topicManager.addSubscribe("$share/group1/test/123", "client1", 1); + List subscribeList = topicManager.getSubscriptions("client1"); + Assertions.assertEquals(4, subscribeList.size()); + } + + @Test + void testRemove() { + TrieTopicManager topicManager = new TrieTopicManager(); + topicManager.addSubscribe("test/123", "client1", 0); + topicManager.addSubscribe("$queue/test/123", "client1", 0); + topicManager.addSubscribe("$share/group1/test/123", "client1", 0); + topicManager.removeSubscribe("$queue/test/123", "client1"); + List subscribeList = topicManager.getSubscriptions("client1"); + Assertions.assertEquals(2, subscribeList.size()); + } + + @Test + void testSearch() { + TrieTopicManager topicManager = new TrieTopicManager(); + topicManager.addSubscribe("test/+", "client1", 0); + topicManager.addSubscribe("test/+/", "client2", 1); + topicManager.addSubscribe("test/+/1", "client3", 1); + topicManager.addSubscribe("$queue/test/#", "client4", 0); + topicManager.addSubscribe("$share/group1/test/123", "client5", 0); + List subscribeList = topicManager.getSubscriptions("client1"); + Assertions.assertEquals(1, subscribeList.size()); + List subscribes = topicManager.searchSubscribe("test/123"); + System.out.println(subscribes); + } + + @Test + void test() { + TrieTopicManager topicManager = new TrieTopicManager(); + topicManager.addSubscribe("test/123", "client1", 1); + topicManager.addSubscribe("test/1234", "client1", 1); + topicManager.addSubscribe("test/1235", "client1", 1); + topicManager.addSubscribe("test1/123", "client1", 1); + topicManager.addSubscribe("+/123", "client1", 0); + topicManager.addSubscribe("test/#", "client1", 1); + topicManager.addSubscribe("/test/123", "client1", 0); + topicManager.addSubscribe("$share/group1/test/123", "client2", 0); + topicManager.addSubscribe("$queue/test/123", "client3", 0); + + List subscribeList = topicManager.searchSubscribe("test/123"); + Assertions.assertFalse(subscribeList.isEmpty()); + + List subscriptions = topicManager.getSubscriptions("client1"); + Assertions.assertFalse(subscriptions.isEmpty()); + + topicManager.removeSubscribe("/test/123", "client1"); + topicManager.removeSubscribe("client1"); + subscriptions = topicManager.getSubscriptions("client1"); + Assertions.assertTrue(subscriptions.isEmpty()); + } + +} diff --git a/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest1.java b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest1.java new file mode 100644 index 0000000..207d906 --- /dev/null +++ b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest1.java @@ -0,0 +1,36 @@ +package org.dromara.mica.mqtt.core.server.test.cluster; + +import org.tio.server.cluster.core.ClusterApi; +import org.tio.server.cluster.core.ClusterConfig; +import org.tio.server.cluster.core.ClusterImpl; + +import java.nio.charset.StandardCharsets; + +/** + * 集群开发测试 + * + * @author L.cm + */ +public class ClusterTest1 { + + public static void main(String[] args) throws Exception { + ClusterConfig config = new ClusterConfig("127.0.0.1", 3001, message -> { + System.out.println(new String(message.getPayload())); + }); + + config.addSeedMember("127.0.0.1", 3001); + config.addSeedMember("127.0.0.1", 3002); + config.addSeedMember("127.0.0.1", 3003); + + // TODO L.cm 思考:是不是不应该无限的对离线重试,对方重新连接触发再重新连接 + + ClusterApi cluster = new ClusterImpl(config); + cluster.start(); + + cluster.schedule(() -> { + String message = String.format("hello mica form cluster:%s ns:%s", cluster.getLocalMember(), System.nanoTime()); + cluster.broadcast(message.getBytes(StandardCharsets.UTF_8)); + }, 3000); + } + +} diff --git a/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest2.java b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest2.java new file mode 100644 index 0000000..773912c --- /dev/null +++ b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest2.java @@ -0,0 +1,34 @@ +package org.dromara.mica.mqtt.core.server.test.cluster; + +import org.tio.server.cluster.core.ClusterApi; +import org.tio.server.cluster.core.ClusterConfig; +import org.tio.server.cluster.core.ClusterImpl; + +import java.nio.charset.StandardCharsets; + +/** + * 集群开发测试 + * + * @author L.cm + */ +public class ClusterTest2 { + + public static void main(String[] args) throws Exception { + ClusterConfig config = new ClusterConfig("127.0.0.1", 3002, message -> { + System.out.println(new String(message.getPayload())); + }); + + config.addSeedMember("127.0.0.1", 3001); + config.addSeedMember("127.0.0.1", 3002); + config.addSeedMember("127.0.0.1", 3003); + + ClusterApi cluster = new ClusterImpl(config); + cluster.start(); + + cluster.schedule(() -> { + String message = String.format("hello mica form cluster:%s ns:%s", cluster.getLocalMember(), System.nanoTime()); + cluster.broadcast(message.getBytes(StandardCharsets.UTF_8)); + }, 3000); + } + +} diff --git a/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest3.java b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest3.java new file mode 100644 index 0000000..b864167 --- /dev/null +++ b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest3.java @@ -0,0 +1,34 @@ +package org.dromara.mica.mqtt.core.server.test.cluster; + +import org.tio.server.cluster.core.ClusterApi; +import org.tio.server.cluster.core.ClusterConfig; +import org.tio.server.cluster.core.ClusterImpl; + +import java.nio.charset.StandardCharsets; + +/** + * 集群开发测试 + * + * @author L.cm + */ +public class ClusterTest3 { + + public static void main(String[] args) throws Exception { + ClusterConfig config = new ClusterConfig("127.0.0.1", 3003, message -> { + System.out.println(new String(message.getPayload())); + }); + + config.addSeedMember("127.0.0.1", 3001); + config.addSeedMember("127.0.0.1", 3002); + config.addSeedMember("127.0.0.1", 3003); + + ClusterApi cluster = new ClusterImpl(config); + cluster.start(); + + cluster.schedule(() -> { + String message = String.format("hello mica form cluster:%s ns:%s", cluster.getLocalMember(), System.nanoTime()); + cluster.broadcast(message.getBytes(StandardCharsets.UTF_8)); + }, 3000); + } + +} diff --git a/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest4.java b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest4.java new file mode 100644 index 0000000..d42172c --- /dev/null +++ b/mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest4.java @@ -0,0 +1,35 @@ +package org.dromara.mica.mqtt.core.server.test.cluster; + +import org.tio.server.cluster.core.ClusterApi; +import org.tio.server.cluster.core.ClusterConfig; +import org.tio.server.cluster.core.ClusterImpl; + +import java.nio.charset.StandardCharsets; + +/** + * 集群开发测试 + * + * @author L.cm + */ +public class ClusterTest4 { + + public static void main(String[] args) throws Exception { + ClusterConfig config = new ClusterConfig("127.0.0.1", 3004, message -> { + System.out.println(new String(message.getPayload())); + }); + + // 不在种子成员里 + config.addSeedMember("127.0.0.1", 3001); + config.addSeedMember("127.0.0.1", 3002); + config.addSeedMember("127.0.0.1", 3003); + + ClusterApi cluster = new ClusterImpl(config); + cluster.start(); + + cluster.schedule(() -> { + String message = String.format("hello mica form cluster:%s ns:%s", cluster.getLocalMember(), System.nanoTime()); + cluster.broadcast(message.getBytes(StandardCharsets.UTF_8)); + }, 3000); + } + +} diff --git a/mica-mqtt-server/src/test/resources/tinylog.properties b/mica-mqtt-server/src/test/resources/tinylog.properties new file mode 100644 index 0000000..2ece10d --- /dev/null +++ b/mica-mqtt-server/src/test/resources/tinylog.properties @@ -0,0 +1,7 @@ +writer = console +writer.format = {date: HH:mm:ss.SSS} [{thread}] {level} {class-name}.{method} : {message} +writer.level = info +# level +level@org.tio = info +level@org.tio.client.TioClient = info +level@org.tio.server = info diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8fb4b68 --- /dev/null +++ b/pom.xml @@ -0,0 +1,404 @@ + + + 4.0.0 + org.dromara.mica-mqtt + mica-mqtt + ${revision} + pom + + ${project.artifactId} + Mica mqtt client and server! + https://mica-mqtt.dreamlu.net + + + + 2.5.9 + + 1.8 + UTF-8 + 3.5.0 + 1.7.3 + + 1.2.4 + 2.3.5 + 2.7.18 + 3.7.2 + 5.2.7 + 2.7.0 + 2.5 + + + + mica-mqtt-codec + mica-mqtt-common + mica-mqtt-client + mica-mqtt-server + starter + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + net.dreamlu + mica-net-core + ${mica-net.version} + + + net.dreamlu + mica-net-http + ${mica-net.version} + + + net.dreamlu + mica-auto + ${mica-auto.version} + provided + + + org.noear + solon + ${solon.version} + + + org.noear + solon-web + ${solon.version} + + + org.noear + solon-logging-simple + ${solon.version} + + + org.noear + solon-scheduling-simple + ${solon.version} + + + org.noear + solon-cloud-metrics + ${solon.version} + + + com.jfinal + jfinal + ${jfinal.version} + + + org.dromara.mica-mqtt + mica-mqtt-codec + ${revision} + + + org.dromara.mica-mqtt + mica-mqtt-common + ${revision} + + + org.dromara.mica-mqtt + mica-mqtt-client + ${revision} + + + org.dromara.mica-mqtt + mica-mqtt-server + ${revision} + + + org.dromara.mica-mqtt + mica-mqtt-server-spring-boot-starter + ${revision} + + + org.dromara.mica-mqtt + mica-mqtt-client-spring-boot-starter + ${revision} + + + org.dromara.mica-mqtt + mica-mqtt-client-solon-plugin + ${revision} + + + org.dromara.mica-mqtt + mica-mqtt-server-solon-plugin + ${revision} + + + + org.tinylog + slf4j-tinylog + ${tinylog.version} + + + org.tinylog + tinylog-impl + ${tinylog.version} + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + + + + ChunmengLu + qq596392912@gmail.com + + + + + scm:git:git@gitee.com/dromara/mica-mqtt + scm:git:git@gitee.com/dromara/mica-mqtt.git + git@gitee.com/dromara/mica-mqtt.git + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + true + + + + + org.apache.maven.plugins + maven-source-plugin + 3.4.0 + + + attach-sources + verify + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + org.codehaus.mojo + flatten-maven-plugin + ${maven-flatten.version} + + true + oss + + remove + remove + + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + true + + + true + true + true + + + false + + + + + org.moditect + moditect-maven-plugin + 1.3.0 + + + add-module-info + package + + add-module-info + + + 9 + + ${project.basedir}/src/main/moditect/module-info.java + + + true + + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + true + + + + + + + + + Apache License 2.0 + https://www.apache.org/licenses + + + + + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + + + + develop + + true + + + mica-mqtt-common + mica-mqtt-client + mica-mqtt-server + starter + system + + + + aliyun + aliyun + https://maven.aliyun.com/repository/public/ + + false + + + true + + + + + + snapshot + + + central + https://central.sonatype.com/repository/maven-snapshots/ + + + + + + + org.sonatype.central + central-publishing-maven-plugin + + + + + + release + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + + package + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + verify + + sign + + + + + + --pinentry-mode + loopback + + + + + + org.sonatype.central + central-publishing-maven-plugin + + + + + + + diff --git a/starter/README.md b/starter/README.md new file mode 100644 index 0000000..5203102 --- /dev/null +++ b/starter/README.md @@ -0,0 +1,16 @@ +# 插件 + +## Spring boot + +- [mica-mqtt-client-spring-boot-starter 使用文档](mica-mqtt-client-spring-boot-starter/README.md) +- [mica-mqtt-server-spring-boot-starter 使用文档](mica-mqtt-server-spring-boot-starter/README.md) + +## solon + +- [mica-mqtt-client-solon-plugin 使用文档](mica-mqtt-client-solon-plugin/README.md) +- [mica-mqtt-server-solon-plugin 使用文档](mica-mqtt-server-solon-plugin/README.md) + +## JFinal + +- [mica-mqtt-client-jfinal-plugin 使用文档](mica-mqtt-client-jfinal-plugin/README.md) +- [mica-mqtt-server-jfinal-plugin 使用文档](mica-mqtt-server-jfinal-plugin/README.md) diff --git a/starter/mica-mqtt-client-jfinal-plugin/README.md b/starter/mica-mqtt-client-jfinal-plugin/README.md new file mode 100644 index 0000000..9817d93 --- /dev/null +++ b/starter/mica-mqtt-client-jfinal-plugin/README.md @@ -0,0 +1,81 @@ +# jfinal mica-mqtt client + +## 使用 + +#### 1. 添加依赖 +```xml + + org.dromara.mica-mqtt + mica-mqtt-client-jfinal-plugin + ${最新版本} + +``` + +#### 2. 删除 jfinal-demo 中的 slf4j-nop 依赖 + +#### 3. 添加 slf4j-log4j12 +```xml + + org.slf4j + slf4j-log4j12 + 1.7.33 + +``` + +#### 4. 在 jfinal Config configPlugin 中添加 mica-mqtt client 插件 +```java +MqttClientPlugin mqttClientPlugin = new MqttClientPlugin(); +mqttClientPlugin.config(mqttClientCreator -> { + // 设置 mqtt 连接配置信息 + mqttClientCreator + .clientId("clientId") // 按需配置,相同的会互踢 + .ip("mqtt.dreamlu.net") + .port(1883) + .connectListener(Aop.get(MqttClientConnectListener.class)); +}); +me.add(mqttClientPlugin); +``` + +#### 5. 在 jfinal Config onStart 启动完成之后添加 mqtt 订阅 +```java +@Override +public void onStart() { + IMqttClientMessageListener clientMessageListener = Aop.get(TestMqttClientMessageListener.class); + MqttClientKit.subQos0("#", clientMessageListener); +} +``` + +#### 6. 使用 MqttClientKit 发送消息 +```java +MqttClientKit.publish("mica", "hello".getBytes(StandardCharsets.UTF_8)); +``` + +### 7. 示例代码 MqttClientConnectListener +```java +public class MqttClientConnectListener implements IMqttClientConnectListener { + + @Override + public void onConnected(ChannelContext channelContext, boolean isReconnect) { + if (isReconnect) { + System.out.println("重连 mqtt 服务器重连成功..."); + } else { + System.out.println("连接 mqtt 服务器成功..."); + } + } + + @Override + public void onDisconnect(ChannelContext channelContext, Throwable throwable, String remark, boolean isRemove) { + System.out.println("mqtt 链接断开 remark:" + remark + " isRemove:" + isRemove); + } +} +``` + +### 8. 示例 TestMqttClientMessageListener +```java +public class TestMqttClientMessageListener implements IMqttClientMessageListener { + @Override + public void onMessage(String topic, MqttPublishMessage message, byte[] payload) { + System.out.println("收到消息 topic:" + topic + "内容:\n" + new String(payload, StandardCharsets.UTF_8)); + } +} +``` \ No newline at end of file diff --git a/starter/mica-mqtt-client-jfinal-plugin/pom.xml b/starter/mica-mqtt-client-jfinal-plugin/pom.xml new file mode 100644 index 0000000..4a97245 --- /dev/null +++ b/starter/mica-mqtt-client-jfinal-plugin/pom.xml @@ -0,0 +1,32 @@ + + + + 4.0.0 + + org.dromara.mica-mqtt + starter + ${revision} + + mica-mqtt-client-jfinal-plugin + ${project.artifactId} + https://mica-mqtt.dreamlu.net/guide/jfinal/client.html + + + + org.dromara.mica-mqtt + mica-mqtt-client + + + com.jfinal + jfinal + provided + + + org.junit.jupiter + junit-jupiter-engine + test + + + + diff --git a/starter/mica-mqtt-client-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/client/MqttClientKit.java b/starter/mica-mqtt-client-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/client/MqttClientKit.java new file mode 100644 index 0000000..63f0573 --- /dev/null +++ b/starter/mica-mqtt-client-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/client/MqttClientKit.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.jfinal.client; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.builder.MqttPublishBuilder; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.core.client.IMqttClientMessageListener; +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.dromara.mica.mqtt.core.client.MqttClientCreator; +import org.dromara.mica.mqtt.core.client.MqttClientSubscription; +import org.tio.client.ClientChannelContext; +import org.tio.client.TioClient; +import org.tio.client.TioClientConfig; + +import java.util.List; +import java.util.function.Consumer; + +/** + * mica mqtt client kit + * + * @author L.cm + * @author ChangJin Wei (魏昌进) + */ +public class MqttClientKit { + private static MqttClient client; + + /** + * 初始化 + * + * @param client MqttClient + */ + static void init(MqttClient client) { + MqttClientKit.client = client; + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public static MqttClient subQos0(String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS0, listener, null); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param properties MqttProperties + * @param listener MqttMessageListener + * @return MqttClient + */ + public static MqttClient subQos0(String topicFilter, MqttProperties properties, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS0, listener, properties); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public static MqttClient subQos1(String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS1, listener, null); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param properties MqttProperties + * @param listener MqttMessageListener + * @return MqttClient + */ + public static MqttClient subQos1(String topicFilter, MqttProperties properties, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS1, listener, properties); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public static MqttClient subQos2(String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS2, listener, null); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param properties MqttProperties + * @param listener MqttMessageListener + * @return MqttClient + */ + public static MqttClient subQos2(String topicFilter, MqttProperties properties, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS2, listener, properties); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public static MqttClient subscribe(MqttQoS mqttQoS, String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, mqttQoS, listener, null); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public static MqttClient subscribe(String topicFilter, MqttQoS mqttQoS, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, mqttQoS, listener, null); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @param properties MqttProperties + * @return MqttClient + */ + public static MqttClient subscribe(String topicFilter, MqttQoS mqttQoS, IMqttClientMessageListener listener, MqttProperties properties) { + return client.subscribe(topicFilter, mqttQoS, listener, properties); + } + + /** + * 订阅 + * + * @param topicFilters topicFilter 数组 + * @param mqttQoS MqttQoS + * @param listener MqttMessageListener + * @return MqttClient + */ + public static MqttClient subscribe(String[] topicFilters, MqttQoS mqttQoS, IMqttClientMessageListener listener) { + return client.subscribe(topicFilters, mqttQoS, listener, null); + } + + /** + * 订阅 + * + * @param topicFilters topicFilter 数组 + * @param mqttQoS MqttQoS + * @param listener MqttMessageListener + * @param properties MqttProperties + * @return MqttClient + */ + public static MqttClient subscribe(String[] topicFilters, MqttQoS mqttQoS, IMqttClientMessageListener listener, MqttProperties properties) { + return client.subscribe(topicFilters, mqttQoS, listener, properties); + } + + /** + * 批量订阅 + * + * @param subscriptionList 订阅集合 + * @return MqttClient + */ + public static MqttClient subscribe(List subscriptionList) { + return client.subscribe(subscriptionList, null); + } + + /** + * 批量订阅 + * + * @param subscriptionList 订阅集合 + * @param properties MqttProperties + * @return MqttClient + */ + public static MqttClient subscribe(List subscriptionList, MqttProperties properties) { + return client.subscribe(subscriptionList, properties); + } + + /** + * 取消订阅 + * + * @param topicFilters topicFilter 集合 + * @return MqttClient + */ + public static MqttClient unSubscribe(String... topicFilters) { + return client.unSubscribe(topicFilters); + } + + /** + * 取消订阅 + * + * @param topicFilters topicFilter 集合 + * @return MqttClient + */ + public static MqttClient unSubscribe(List topicFilters) { + return client.unSubscribe(topicFilters); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @return 是否发送成功 + */ + public static boolean publish(String topic, Object payload) { + return client.publish(topic, payload); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public static boolean publish(String topic, Object payload, MqttQoS qos) { + return client.publish(topic, payload, qos); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public static boolean publish(String topic, Object payload, boolean retain) { + return client.publish(topic, payload, retain); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public static boolean publish(String topic, Object payload, MqttQoS qos, boolean retain) { + return client.publish(topic, payload, qos, retain); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param builder PublishBuilder + * @return 是否发送成功 + */ + public static boolean publish(String topic, Object payload, MqttQoS qos, Consumer builder) { + return client.publish(topic, payload, qos, builder); + } + + /** + * 重连 + */ + public static void reconnect() { + client.reconnect(); + } + + /** + * 断开 mqtt 连接 + * + * @return 是否成功 + */ + public static boolean disconnect() { + return client.disconnect(); + } + + /** + * 获取 TioClient + * + * @return TioClient + */ + public static TioClient getTioClient() { + return client.getTioClient(); + } + + /** + * 获取配置 + * + * @return MqttClientCreator + */ + public static MqttClientCreator getClientCreator() { + return client.getClientCreator(); + } + + /** + * 获取 TioClientConfig + * + * @return TioClientConfig + */ + public static TioClientConfig getClientTioConfig() { + return client.getClientTioConfig(); + } + + /** + * 获取 ClientChannelContext + * + * @return ClientChannelContext + */ + public static ClientChannelContext getContext() { + return client.getContext(); + } + + /** + * 判断客户端跟服务端是否连接 + * + * @return 是否已经连接成功 + */ + public static boolean isConnected() { + return client.isConnected(); + } + + /** + * 判断客户端跟服务端是否断开连接 + * + * @return 是否断连 + */ + public static boolean isDisconnected() { + return client.isDisconnected(); + } + +} diff --git a/starter/mica-mqtt-client-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/client/MqttClientPlugin.java b/starter/mica-mqtt-client-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/client/MqttClientPlugin.java new file mode 100644 index 0000000..3c696d2 --- /dev/null +++ b/starter/mica-mqtt-client-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/client/MqttClientPlugin.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.jfinal.client; + +import com.jfinal.plugin.IPlugin; +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.dromara.mica.mqtt.core.client.MqttClientCreator; + +import java.util.function.Consumer; + +/** + * mica mqtt client 插件 + * + * @author L.cm + */ +public class MqttClientPlugin implements IPlugin { + private final MqttClientCreator clientCreator; + private MqttClient mqttClient; + + public MqttClientPlugin() { + this.clientCreator = new MqttClientCreator(); + } + + /** + * 配置 mica mqtt + * + * @param consumer MqttClientCreator Consumer + */ + public void config(Consumer consumer) { + consumer.accept(this.clientCreator); + } + + @Override + public boolean start() { + if (this.mqttClient == null) { + // 连接超时时间,如果没设置,改成 3s,减少因连不上卡顿时间 + Integer timeout = clientCreator.getTimeout(); + if (timeout == null) { + clientCreator.timeout(3); + } + // 使用同步连接 + this.mqttClient = clientCreator.connectSync(); + } else { + this.mqttClient.reconnect(); + } + MqttClientKit.init(this.mqttClient); + return true; + } + + @Override + public boolean stop() { + if (this.mqttClient != null) { + this.mqttClient.stop(); + } + return true; + } + +} diff --git a/starter/mica-mqtt-client-jfinal-plugin/src/main/moditect/module-info.java b/starter/mica-mqtt-client-jfinal-plugin/src/main/moditect/module-info.java new file mode 100644 index 0000000..03ad704 --- /dev/null +++ b/starter/mica-mqtt-client-jfinal-plugin/src/main/moditect/module-info.java @@ -0,0 +1,5 @@ +open module org.dromara.mica.mqtt.client.jfinal.plugin { + requires jfinal; + requires transitive org.dromara.mica.mqtt.client; + exports org.dromara.mica.mqtt.jfinal.client; +} diff --git a/starter/mica-mqtt-client-jfinal-plugin/src/test/java/org/dromara/mica/mqtt/jfinal/client/MqttClientPluginTest.java b/starter/mica-mqtt-client-jfinal-plugin/src/test/java/org/dromara/mica/mqtt/jfinal/client/MqttClientPluginTest.java new file mode 100644 index 0000000..b27b429 --- /dev/null +++ b/starter/mica-mqtt-client-jfinal-plugin/src/test/java/org/dromara/mica/mqtt/jfinal/client/MqttClientPluginTest.java @@ -0,0 +1,19 @@ +package org.dromara.mica.mqtt.jfinal.client; + +/** + * mica mqtt client 插件测试 + * + * @author L.cm + */ +public class MqttClientPluginTest { + + public static void main(String[] args) { + MqttClientPlugin plugin = new MqttClientPlugin(); + plugin.config(mqttClientCreator -> { + // mqttClientCreator 上有很多方法,详见 mica-mqtt-core + mqttClientCreator.port(1883).username("mica").password("mica"); + }); + plugin.start(); + } + +} diff --git a/starter/mica-mqtt-client-solon-plugin/README.md b/starter/mica-mqtt-client-solon-plugin/README.md new file mode 100644 index 0000000..b1d811b --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/README.md @@ -0,0 +1,197 @@ +# mica-mqtt-client-solon-plugin 使用文档 + +本插件基于 https://gitee.com/peigenlpy/mica-mqtt-solon-plugin 调整合并到官方(已经过作者同意)。 + +## 版本兼容 +| 要求 | Solon 版本 | +|-----|-------| +| 最高 | 3.x | +| 最低 | 2.8.0 | + +## 一、添加依赖 + +```xml + + org.dromara.mica-mqtt + mica-mqtt-client-solon-plugin + ${version} + +``` + +## 二、mqtt 客户端 + +### 2.1 配置项示例 +```yaml +mqtt: + client: + enabled: true # 是否开启客户端,默认:true + ip: 127.0.0.1 # 连接的服务端 ip ,默认:127.0.0.1 + port: 1883 # 端口:默认:1883 + name: Mica-Mqtt-Client # 名称,默认:Mica-Mqtt-Client + clientId: 000001 # 客户端Id(非常重要,一般为设备 sn,不可重复) + username: mica # 认证的用户名,注意:2.5.x 开始将 user-name 改成了 username + password: 123456 # 认证的密码 + timeout: 5 # 超时时间,单位:秒,默认:5秒 + reconnect: true # 是否重连,默认:true + re-interval: 5000 # 重连时间,默认 5000 毫秒 + version: mqtt_3_1_1 # mqtt 协议版本,可选 MQTT_3_1、mqtt_3_1_1、mqtt_5,默认:mqtt_3_1_1 + read-buffer-size: 8KB # 接收数据的 buffer size,默认:8k + max-bytes-in-message: 10MB # 消息解析最大 bytes 长度,默认:10M + keep-alive-secs: 60 # keep-alive 时间,单位:秒 + heartbeat-mode: LAST_REQ # 心跳模式,支持最后发送或接收心跳时间来计算心跳,默认:最后发送心跳的时间。(2.4.3 开始支持) + heartbeat-timeout-strategy: PING # 心跳超时策略,支持发送 PING 和 CLOSE 断开连接,默认:最大努力发送 PING。(2.4.3 开始支持) + clean-start: true # session 保留 2.5.x 使用 clean-start,老版本用 clean-session,默认:true + session-expiry-interval-secs: 0 # 开启保留 session 时,session 的有效期,默认:0(2.4.2 开始支持) + biz-thread-pool-size: 2 # mqtt 工作线程数,默认:2,如果消息量比较大,处理较慢,例如做 emqx 的转发消息处理,可以调大此参数(2.4.2 开始支持) + ssl: + enabled: false # 是否开启 ssl 认证,2.1.0 开始支持双向认证 + keystore-path: # 可选参数:ssl 双向认证 keystore 目录,支持 classpath:/ 路径。 + keystore-pass: # 可选参数:ssl 双向认证 keystore 密码 + truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 + truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 +``` + +注意:**ssl** 存在三种情况 + +| 服务端开启ssl | 客户端 | +| ---------------------------------------- | --------------------------------------------- | +| ClientAuth 为 NONE(不需要客户端验证) | 仅仅需要开启 ssl 即可不用配置证书 | +| ClientAuth 为 OPTIONAL(与客户端协商) | 需开启 ssl 并且配置 truststore 证书 | +| ClientAuth 为 REQUIRE (必须的客户端验证) | 需开启 ssl 并且配置 truststore、 keystore证书 | + + +### 2.2 可实现接口(注册成 Solon Bean 即可) + +| 接口 | 是否必须 | 说明 | +| --------------------------- |------| ------------------------- | +| IMqttClientConnectListener | 否 | 客户端连接成功监听 | + +### 2.3 客户端上下线监听 +使用 Solon event 解耦客户端上下线监听,注意: 会跟自定义的 `IMqttClientConnectListener` 实现冲突,取一即可。 + +```java +@Component +public class MqttClientConnectedListener implements EventListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientConnectedListener.class); + + @Inject + private MqttClientCreator mqttClientCreator; + + @Override + public void onEvent(MqttConnectedEvent mqttConnectedEvent) throws Throwable { + logger.info("MqttConnectedEvent:{}", mqttConnectedEvent); + } +} +``` +```java +@Component +public class MqttClientDisconnectListener implements EventListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientDisconnectListener.class); + + @Inject + private MqttClientCreator mqttClientCreator; + + @Override + public void onEvent(MqttDisconnectEvent mqttDisconnectEvent) throws Throwable { + logger.info("MqttDisconnectEvent:{}", mqttDisconnectEvent); + // 在断线时更新 clientId、username、password + mqttClientCreator.clientId("newClient" + System.currentTimeMillis()) + .username("newUserName") + .password("newPassword"); + } +} + +``` + +### 2.4 自定义 java 配置(可选) + +```java +@Configuration +public class MqttClientCustomizerConfiguration { + + @Bean + public MqttClientCustomizer mqttClientCustomizer() { + return new MqttClientCustomizer() { + @Override + public void customize(MqttClientCreator creator) { + // 此处可自定义配置 creator,会覆盖 yml 中的配置 + System.out.println("----------------MqttServerCustomizer-----------------"); + } + }; + } + +} +``` + +### 2.5 订阅示例 +```java +/** + * 客户端消息监听 + */ +@Component +public class MqttClientSubscribeListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientSubscribeListener.class); + + @MqttClientSubscribe("/test/#") + public void subQos0(String topic, byte[] payload) { + logger.info("subQos0,topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + + @MqttClientSubscribe(value = "/qos1/#", qos = MqttQoS.AT_LEAST_ONCE) + public void subQos1(String topic, byte[] payload) { + logger.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + + @MqttClientSubscribe("/sys/${productKey}/${deviceName}/thing/sub/register") + public void thingSubRegister(String topic, byte[] payload) { + // 1.3.8 开始支持,@MqttClientSubscribe 注解支持 ${} 变量替换,会默认替换成 + + // 注意:mica-mqtt 会先从 Spring boot 配置中替换参数 ${},如果存在配置会优先被替换。 + logger.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + +} +``` +```java +/** + * 客户端消息监听的另一种方式 + */ +@MqttClientSubscribe("${topic1}") +public class MqttClientMessageListener implements IMqttClientMessageListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientMessageListener.class); + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + logger.info("MqttClientMessageListener,topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } +} +``` + +### 2.6 共享订阅 topic 说明 +mica-mqtt client 支持**两种共享订阅**方式: + +1. 共享订阅:订阅前缀 `$queue/`,多个客户端订阅了 `$queue/topic`,发布者发布到topic,则只有一个客户端会接收到消息。 +2. 分组订阅:订阅前缀 `$share//`,组客户端订阅了`$share/group1/topic`、`$share/group2/topic`..,发布者发布到topic,则消息会发布到每个group中,但是每个group中只有一个客户端会接收到消息。 + +### 2.8 MqttClientTemplate 使用示例 + +```java +@Component +public class ClientService { + private static final Logger logger = LoggerFactory.getLogger(ClientService.class); + @Inject + private MqttClientTemplate client; + + public boolean publish(String body) { + client.publish("/test/client", body.getBytes(StandardCharsets.UTF_8)); + return true; + } + + public boolean sub() { + client.subQos0("/test/#", (context, topic, message, payload) -> { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + }); + return true; + } + +} +``` diff --git a/starter/mica-mqtt-client-solon-plugin/pom.xml b/starter/mica-mqtt-client-solon-plugin/pom.xml new file mode 100644 index 0000000..c1b9e89 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + starter + ${revision} + + mica-mqtt-client-solon-plugin + ${project.artifactId} + https://mica-mqtt.dreamlu.net/guide/solon/client.html + + + + org.dromara.mica-mqtt + mica-mqtt-client + + + org.noear + solon + provided + + + org.noear + solon-web + test + + + org.noear + solon-logging-simple + test + + + org.projectlombok + lombok + provided + + + + diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientSubscribeListener.java b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientSubscribeListener.java new file mode 100644 index 0000000..49831b8 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientSubscribeListener.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.client.solon; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.client.IMqttClientMessageListener; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.function.ParamValueFunction; +import org.dromara.mica.mqtt.core.util.MethodParamUtil; +import org.tio.core.ChannelContext; +import org.tio.utils.mica.ExceptionUtils; + +import java.lang.reflect.Method; + +/** + * MqttClientSubscribe 注解订阅监听 + * + * @author L.cm + */ +@Slf4j +public class MqttClientSubscribeListener implements IMqttClientMessageListener { + private final Object bean; + private final Method method; + private final ParamValueFunction[] paramValueFunctions; + + public MqttClientSubscribeListener(Object bean, Method method, String[] topicTemplates, String[] topicFilters, MqttDeserializer deserializer) { + this.bean = bean; + this.method = method; + this.paramValueFunctions = MethodParamUtil.getParamValueFunctions(method, topicTemplates, topicFilters, deserializer); + } + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + // 获取方法参数 + Object[] args = getMethodParameters(context, topic, message, payload); + // 方法调用 + try { + method.invoke(bean, args); + } catch (Throwable e) { + throw ExceptionUtils.unchecked(e); + } + } + + /** + * 获取反射参数 + * + * @param context context + * @param topic topic + * @param message message + * @param payload payload + * @return Object array + */ + protected Object[] getMethodParameters(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + int length = paramValueFunctions.length; + Object[] parameters = new Object[length]; + for (int i = 0; i < length; i++) { + ParamValueFunction function = paramValueFunctions[i]; + parameters[i] = function.getValue(context, topic, message, payload); + } + return parameters; + } + +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientTemplate.java b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientTemplate.java new file mode 100644 index 0000000..48e63a7 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientTemplate.java @@ -0,0 +1,458 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.client.solon; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.codec.message.builder.MqttPublishBuilder; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.client.*; +import org.tio.client.ClientChannelContext; +import org.tio.client.TioClient; +import org.tio.client.TioClientConfig; +import org.tio.utils.timer.TimerTask; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * mqtt client 模板 + * + * @author wsq(冷月宫主) + * @author ChangJin Wei (魏昌进) + */ +@Slf4j +public class MqttClientTemplate { + private final MqttClientCreator clientCreator; + private final IMqttClientConnectListener clientConnectListener; + private final MqttClientCustomizer customizer; + private final List tempSubscriptionList; + private MqttClient client; + + public MqttClientTemplate(MqttClientCreator clientCreator) { + this(clientCreator, null, null); + } + + public MqttClientTemplate(MqttClientCreator clientCreator, IMqttClientConnectListener clientConnectListener) { + this(clientCreator, clientConnectListener, null); + } + + public MqttClientTemplate(MqttClientCreator clientCreator, + IMqttClientConnectListener clientConnectListener, + MqttClientCustomizer customizer) { + this.clientCreator = clientCreator; + this.clientConnectListener = clientConnectListener; + this.customizer = customizer; + this.tempSubscriptionList = new ArrayList<>(); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subQos0(String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS0, listener); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subQos1(String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS1, listener); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subQos2(String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS2, listener); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subscribe(MqttQoS mqttQoS, String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(mqttQoS, topicFilter, listener); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subscribe(String topicFilter, MqttQoS mqttQoS, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, mqttQoS, listener); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @param properties MqttProperties + * @return MqttClient + */ + public MqttClient subscribe(String topicFilter, MqttQoS mqttQoS, IMqttClientMessageListener listener, MqttProperties properties) { + return client.subscribe(topicFilter, mqttQoS, listener, properties); + } + + /** + * 订阅 + * + * @param topicFilters topicFilter 数组 + * @param mqttQoS MqttQoS + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subscribe(String[] topicFilters, MqttQoS mqttQoS, IMqttClientMessageListener listener) { + return client.subscribe(topicFilters, mqttQoS, listener); + } + + /** + * 订阅 + * + * @param topicFilters topicFilter 数组 + * @param mqttQoS MqttQoS + * @param listener MqttMessageListener + * @param properties MqttProperties + * @return MqttClient + */ + public MqttClient subscribe(String[] topicFilters, MqttQoS mqttQoS, IMqttClientMessageListener listener, MqttProperties properties) { + return client.subscribe(topicFilters, mqttQoS, listener, properties); + } + + /** + * 批量订阅 + * + * @param subscriptionList 订阅集合 + * @return MqttClient + */ + public MqttClient subscribe(List subscriptionList) { + return client.subscribe(subscriptionList); + } + + /** + * 批量订阅 + * + * @param subscriptionList 订阅集合 + * @param properties MqttProperties + * @return MqttClient + */ + public MqttClient subscribe(List subscriptionList, MqttProperties properties) { + return client.subscribe(subscriptionList, properties); + } + + /** + * 取消订阅 + * + * @param topicFilters topicFilter 集合 + * @return MqttClient + */ + public MqttClient unSubscribe(String... topicFilters) { + return client.unSubscribe(topicFilters); + } + + /** + * 取消订阅 + * + * @param topicFilters topicFilter 集合 + * @return MqttClient + */ + public MqttClient unSubscribe(List topicFilters) { + return client.unSubscribe(topicFilters); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload) { + return client.publish(topic, payload, MqttQoS.QOS0); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos) { + return client.publish(topic, payload, qos); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, boolean retain) { + return client.publish(topic, payload, retain); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos, boolean retain) { + return client.publish(topic, payload, qos, retain); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @param properties MqttProperties + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos, boolean retain, MqttProperties properties) { + return client.publish(topic, payload, qos, retain, properties); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param builder PublishBuilder + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos, Consumer builder) { + return client.publish(topic, payload, qos, builder); + } + + /** + * 发布消息 + * + * @param builder PublishBuilder + * @return 是否发送成功 + */ + public boolean publish(MqttPublishBuilder builder) { + return client.publish(builder); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay) { + return client.schedule(command, delay); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay, Executor executor) { + return client.schedule(command, delay, executor); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay) { + return client.scheduleOnce(command, delay); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay, Executor executor) { + return client.scheduleOnce(command, delay, executor); + } + + /** + * 重连 + */ + public void reconnect() { + client.reconnect(); + } + + /** + * 重连到新的服务端节点 + * + * @param ip ip + * @param port port + * @return 是否成功 + */ + public boolean reconnect(String ip, int port) { + return client.reconnect(ip, port); + } + + /** + * 断开 mqtt 连接 + * + * @return 是否成功 + */ + public boolean disconnect() { + return client.disconnect(); + } + + /** + * 获取 TioClient + * + * @return TioClient + */ + public TioClient getTioClient() { + return client.getTioClient(); + } + + /** + * 获取配置 + * + * @return MqttClientCreator + */ + public MqttClientCreator getClientCreator() { + return clientCreator; + } + + /** + * 获取 ClientTioConfig + * + * @return ClientTioConfig + */ + public TioClientConfig getClientTioConfig() { + return client.getClientTioConfig(); + } + + /** + * 获取 ClientChannelContext + * + * @return ClientChannelContext + */ + public ClientChannelContext getContext() { + return client.getContext(); + } + + /** + * 判断客户端跟服务端是否连接 + * + * @return 是否已经连接成功 + */ + public boolean isConnected() { + return client.isConnected(); + } + + /** + * 判断客户端跟服务端是否断开连接 + * + * @return 是否断连 + */ + public boolean isDisconnected() { + return client.isDisconnected(); + } + + /** + * 获取 MqttClient + * + * @return MqttClient + */ + public MqttClient getMqttClient() { + return client; + } + + /** + * 添加启动时的临时订阅 + * + * @param topicFilters topicFilters + * @param qos MqttQoS + * @param messageListener IMqttClientMessageListener + */ + public void addSubscriptionList(String[] topicFilters, MqttQoS qos, IMqttClientMessageListener messageListener) { + for (String topicFilter : topicFilters) { + tempSubscriptionList.add(new MqttClientSubscription(qos, topicFilter, messageListener)); + } + } + + public void connect() { + // 配置客户端连接监听器 + clientCreator.connectListener(clientConnectListener); + // 自定义处理 + if (customizer != null) { + customizer.customize(clientCreator); + } + // 连接超时时间,如果没设置,改成 3s,减少因连不上卡顿时间 + Integer timeout = clientCreator.getTimeout(); + if (timeout == null) { + clientCreator.timeout(3); + } + // 使用同步连接,不过如果连不上会卡一会 + client = clientCreator.connectSync(); + // 添加订阅并清理零时订阅存储 + client.subscribe(tempSubscriptionList); + tempSubscriptionList.clear(); + log.info("mqtt client connect..."); + } + + public void destroy() { + client.stop(); + } +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/DataSize.java b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/DataSize.java new file mode 100644 index 0000000..ef56d81 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/DataSize.java @@ -0,0 +1,195 @@ +package org.dromara.mica.mqtt.client.solon.config; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.tio.utils.hutool.StrUtil; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * DataSize 兼容 + * + * @author L.cm + */ +@Getter +@RequiredArgsConstructor +class DataSize { + + /** + * Bytes per Kilobyte. + */ + private static final long BYTES_PER_KB = 1024; + + /** + * Bytes per Megabyte. + */ + private static final long BYTES_PER_MB = BYTES_PER_KB * 1024; + + /** + * Bytes per Gigabyte. + */ + private static final long BYTES_PER_GB = BYTES_PER_MB * 1024; + + /** + * Bytes per Terabyte. + */ + private static final long BYTES_PER_TB = BYTES_PER_GB * 1024; + + private final long bytes; + + /** + * Obtain a {@link DataSize} representing the specified number of bytes. + * @param bytes the number of bytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofBytes(long bytes) { + return new DataSize(bytes); + } + + /** + * Obtain a {@link DataSize} representing the specified number of kilobytes. + * @param kilobytes the number of kilobytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofKilobytes(long kilobytes) { + return new DataSize(Math.multiplyExact(kilobytes, BYTES_PER_KB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of megabytes. + * @param megabytes the number of megabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofMegabytes(long megabytes) { + return new DataSize(Math.multiplyExact(megabytes, BYTES_PER_MB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of gigabytes. + * @param gigabytes the number of gigabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofGigabytes(long gigabytes) { + return new DataSize(Math.multiplyExact(gigabytes, BYTES_PER_GB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of terabytes. + * @param terabytes the number of terabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofTerabytes(long terabytes) { + return new DataSize(Math.multiplyExact(terabytes, BYTES_PER_TB)); + } + + /** + * Obtain a {@link DataSize} representing an amount in the specified {@link DataUnit}. + * @param amount the amount of the size, measured in terms of the unit, + * positive or negative + * @return a corresponding {@link DataSize} + */ + public static DataSize of(long amount, DataUnit unit) { + Objects.requireNonNull(unit, "Unit must not be null"); + return new DataSize(Math.multiplyExact(amount, unit.getSize().getBytes())); + } + + /** + * Obtain a {@link DataSize} from a text string such as {@code 12MB} using + * the specified default {@link DataUnit} if no unit is specified. + *

+ * The string starts with a number followed optionally by a unit matching one of the + * supported {@linkplain DataUnit suffixes}. + *

+ * Examples: + *

+	 * "12KB" -- parses as "12 kilobytes"
+	 * "5MB"  -- parses as "5 megabytes"
+	 * "20"   -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
+	 * 
+ * @param text the text to parse + * @return the parsed {@link DataSize} + */ + public static DataSize parse(String text) { + Objects.requireNonNull(text, "Text must not be null"); + try { + Matcher matcher = DataSizeUtils.PATTERN.matcher(text.trim()); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid data size: " + text); + } + DataUnit unit = DataSizeUtils.determineDataUnit(matcher.group(2)); + long amount = Long.parseLong(matcher.group(1)); + return DataSize.of(amount, unit); + } + catch (Exception ex) { + throw new IllegalArgumentException("'" + text + "' is not a valid data size", ex); + } + } + + /** + * Static nested class to support lazy loading of the {@link #PATTERN}. + * @since 5.3.21 + */ + private static class DataSizeUtils { + /** + * The pattern for parsing. + */ + private static final Pattern PATTERN = Pattern.compile("^([+\\-]?\\d+)([a-zA-Z]{0,2})$"); + + private static DataUnit determineDataUnit(String suffix) { + return (StrUtil.isNotBlank(suffix) ? DataUnit.fromSuffix(suffix) : DataUnit.BYTES); + } + } + + @Getter + @RequiredArgsConstructor + public enum DataUnit { + + /** + * Bytes, represented by suffix {@code B}. + */ + BYTES("B", DataSize.ofBytes(1)), + + /** + * Kilobytes, represented by suffix {@code KB}. + */ + KILOBYTES("KB", DataSize.ofKilobytes(1)), + + /** + * Megabytes, represented by suffix {@code MB}. + */ + MEGABYTES("MB", DataSize.ofMegabytes(1)), + + /** + * Gigabytes, represented by suffix {@code GB}. + */ + GIGABYTES("GB", DataSize.ofGigabytes(1)), + + /** + * Terabytes, represented by suffix {@code TB}. + */ + TERABYTES("TB", DataSize.ofTerabytes(1)); + + + private final String suffix; + private final DataSize size; + + /** + * Return the {@link DataUnit} matching the specified {@code suffix}. + * @param suffix one of the standard suffixes + * @return the {@link DataUnit} matching the specified {@code suffix} + * @throws IllegalArgumentException if the suffix does not match the suffix + * of any of this enum's constants + */ + public static DataUnit fromSuffix(String suffix) { + for (DataUnit candidate : values()) { + if (candidate.suffix.equals(suffix)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown data unit suffix '" + suffix + "'"); + } + } + +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/MqttClientConfiguration.java b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/MqttClientConfiguration.java new file mode 100644 index 0000000..ed549b9 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/MqttClientConfiguration.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.client.solon.config; + +import org.dromara.mica.mqtt.client.solon.event.SolonEventMqttClientConnectListener; +import org.dromara.mica.mqtt.core.client.IMqttClientConnectListener; +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.dromara.mica.mqtt.core.client.MqttClientCreator; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.deserialize.MqttJsonDeserializer; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.tio.utils.hutool.StrUtil; + +/** + * mqtt client 配置 + * + * @author L.cm + */ +@Configuration +public class MqttClientConfiguration { + + @Bean + @Condition(onMissingBean = MqttDeserializer.class) + public MqttDeserializer mqttDeserializer() { + return new MqttJsonDeserializer(); + } + + @Bean + @Condition(onMissingBean = IMqttClientConnectListener.class) + public IMqttClientConnectListener solonEventMqttClientConnectListener() { + return new SolonEventMqttClientConnectListener(); + } + + @Bean + public MqttClientCreator mqttClientCreator(MqttClientProperties properties) { + MqttClientCreator clientCreator = MqttClient.create() + .name(properties.getName()) + .ip(properties.getIp()) + .port(properties.getPort()) + .username(properties.getUsername()) + .password(properties.getPassword()) + .clientId(properties.getClientId()) + .bindIp(properties.getBindIp()) + .bindNetworkInterface(properties.getBindNetworkInterface()) + .readBufferSize((int) DataSize.parse(properties.getReadBufferSize()).getBytes()) + .maxBytesInMessage((int) DataSize.parse(properties.getMaxBytesInMessage()).getBytes()) + .maxClientIdLength(properties.getMaxClientIdLength()) + .keepAliveSecs(properties.getKeepAliveSecs()) + .heartbeatMode(properties.getHeartbeatMode()) + .heartbeatTimeoutStrategy(properties.getHeartbeatTimeoutStrategy()) + .reconnect(properties.isReconnect()) + .reInterval(properties.getReInterval()) + .retryCount(properties.getRetryCount()) + .reSubscribeBatchSize(properties.getReSubscribeBatchSize()) + .version(properties.getVersion()) + .cleanStart(properties.isCleanStart()) + .sessionExpiryIntervalSecs(properties.getSessionExpiryIntervalSecs()) + .statEnable(properties.isStatEnable()) + .debug(properties.isDebug()) + .disconnectBeforeStop(properties.isDisconnectBeforeStop()); + Integer timeout = properties.getTimeout(); + if (timeout != null && timeout > 0) { + clientCreator.timeout(timeout); + } + // mqtt 业务线程数 + Integer bizThreadPoolSize = properties.getBizThreadPoolSize(); + if (bizThreadPoolSize != null && bizThreadPoolSize > 0) { + clientCreator.bizThreadPoolSize(bizThreadPoolSize); + } + // 开启 ssl + MqttClientProperties.Ssl ssl = properties.getSsl(); + if (ssl.isEnabled()) { + clientCreator.useSsl(ssl.getKeystorePath(), ssl.getKeystorePass(), ssl.getTruststorePath(), ssl.getTruststorePass()); + } + // 构造遗嘱消息 + MqttClientProperties.WillMessage willMessage = properties.getWillMessage(); + if (willMessage != null && StrUtil.isNotBlank(willMessage.getTopic())) { + clientCreator.willMessage(builder -> { + builder.topic(willMessage.getTopic()) + .qos(willMessage.getQos()) + .retain(willMessage.isRetain()); + if (StrUtil.isNotBlank(willMessage.getMessage())) { + builder.messageText(willMessage.getMessage()); + } + }); + } + return clientCreator; + } + +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/MqttClientProperties.java b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/MqttClientProperties.java new file mode 100644 index 0000000..6f3950d --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/MqttClientProperties.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.client.solon.config; + +import lombok.Getter; +import lombok.Setter; +import org.dromara.mica.mqtt.codec.MqttConstant; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.MqttVersion; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; +import org.tio.client.task.HeartbeatTimeoutStrategy; +import org.tio.core.task.HeartbeatMode; + +/** + * MqttClient 配置 + * + * @author wsq(冷月宫主) + */ +@Getter +@Setter +@Configuration +@Inject(value = "${" + MqttClientProperties.PREFIX + "}", required = false) +public class MqttClientProperties { + + /** + * 配置前缀 + */ + public static final String PREFIX = "mqtt.client"; + /** + * 是否启用,默认:false + */ + private boolean enabled = true; + /** + * 名称,默认:Mica-Mqtt-Client + */ + private String name = "Mica-Mqtt-Client"; + /** + * 服务端 ip,默认:127.0.0.1 + */ + private String ip = "127.0.0.1"; + /** + * 端口,默认:1883 + */ + private int port = 1883; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * 客户端ID + */ + private String clientId; + /** + * 超时时间,单位:秒,t-io 配置,可为 null + */ + private Integer timeout; + /** + * 绑定 ip,绑定网卡,用于多网卡,默认为 null + */ + private String bindIp; + /** + * 绑定网卡,网卡名称,和 bindIp 取其一 + */ + private String bindNetworkInterface; + /** + * 接收数据的 buffer size,默认:8KB + */ + private String readBufferSize = "8KB"; + /** + * 消息解析最大 bytes 长度,默认:10MB + */ + private String maxBytesInMessage = "10MB"; + /** + * mqtt 3.1 会校验此参数为 23,为了减少问题设置成了 64 + */ + private int maxClientIdLength = MqttConstant.DEFAULT_MAX_CLIENT_ID_LENGTH; + /** + * Keep Alive (s) + */ + private int keepAliveSecs = 60; + /** + * 心跳模式,支持最后发送或接收心跳时间来计算心跳,默认:最后发送心跳的时间 + */ + private HeartbeatMode heartbeatMode = HeartbeatMode.LAST_REQ; + /** + * 心跳超时策略,支持发送 PING 和 CLOSE 断开连接,默认:最大努力发送 PING + */ + private HeartbeatTimeoutStrategy heartbeatTimeoutStrategy = HeartbeatTimeoutStrategy.PING; + /** + * 自动重连 + */ + private boolean reconnect = true; + /** + * 重连的间隔时间,单位毫秒,默认:5000 + */ + private long reInterval = 5000; + /** + * 连续重连次数,当连续重连这么多次都失败时,不再重连。0和负数则一直重连 + */ + private int retryCount = 0; + /** + * 重连,重新订阅一个批次大小,默认:20 + */ + private int reSubscribeBatchSize = 20; + /** + * mqtt 协议,默认:MQTT_5 + */ + private MqttVersion version = MqttVersion.MQTT_5; + /** + * 清除会话 + *

+ * false 表示如果订阅的客户机断线了,那么要保存其要推送的消息,如果其重新连接时,则将这些消息推送。 + * true 表示消除,表示客户机是第一次连接,消息所以以前的连接信息。 + *

+ */ + private boolean cleanStart = true; + /** + * 开启保留 session 时,session 的有效期,默认:0 + */ + private int sessionExpiryIntervalSecs = 0; + /** + * 遗嘱消息 + */ + private WillMessage willMessage; + /** + * 是否开启监控,默认:false 不开启,节省内存 + */ + private boolean statEnable = false; + /** + * debug + */ + private boolean debug = false; + /** + * mqtt 工作线程数,默认:2,如果消息量比较大,处理较慢,例如做 emqx 的转发消息处理,可以调大此参数 + */ + private Integer bizThreadPoolSize; + /** + * 停止前是否发送 disconnect 消息,默认:true 不会触发遗嘱消息 + */ + private boolean disconnectBeforeStop = true; + /** + * ssl 配置 + */ + private Ssl ssl = new Ssl(); + + @Getter + @Setter + public static class WillMessage { + /** + * 遗嘱消息 topic + */ + private String topic; + /** + * 遗嘱消息 qos,默认: qos0 + */ + private MqttQoS qos = MqttQoS.QOS0; + /** + * 遗嘱消息 payload + */ + private String message; + /** + * 遗嘱消息保留标识符,默认: false + */ + private boolean retain = false; + } + + @Getter + @Setter + public static class Ssl { + /** + * 启用 ssl + */ + private boolean enabled = false; + /** + * keystore 证书路径 + */ + private String keystorePath; + /** + * keystore 密码 + */ + private String keystorePass; + /** + * truststore 证书路径 + */ + private String truststorePath; + /** + * truststore 密码 + */ + private String truststorePass; + } + +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/MqttConnectedEvent.java b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/MqttConnectedEvent.java new file mode 100644 index 0000000..93b9a62 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/MqttConnectedEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.client.solon.event; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * mqtt 客户端连接成功事件 + * + * @author L.cm + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MqttConnectedEvent implements Serializable { + + /** + * 是否重连 + */ + private boolean isReconnect; + +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/MqttDisconnectEvent.java b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/MqttDisconnectEvent.java new file mode 100644 index 0000000..a706e84 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/MqttDisconnectEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.client.solon.event; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * mqtt 客户端断开连接事件 + * + * @author L.cm + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MqttDisconnectEvent implements Serializable { + + /** + * 断开原因 + */ + String reason; + /** + * 是否删除连接 + */ + boolean isRemove; + +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/SolonEventMqttClientConnectListener.java b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/SolonEventMqttClientConnectListener.java new file mode 100644 index 0000000..09499f8 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/SolonEventMqttClientConnectListener.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.client.solon.event; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.client.IMqttClientConnectListener; +import org.noear.solon.core.event.EventBus; +import org.tio.core.ChannelContext; + +/** + * spring event mqtt client 连接监听 + * + * @author L.cm + */ +@Slf4j +public class SolonEventMqttClientConnectListener implements IMqttClientConnectListener { + + @Override + public void onConnected(ChannelContext context, boolean isReconnect) { + if (isReconnect) { + log.info("重连 mqtt 服务器重连成功..."); + } else { + log.info("连接 mqtt 服务器成功..."); + } + EventBus.publish(new MqttConnectedEvent(isReconnect)); + } + + @Override + public void onDisconnect(ChannelContext channelContext, Throwable throwable, String remark, boolean isRemove) { + String reason; + if (throwable == null) { + reason = remark; + log.info("mqtt 链接断开 remark:{} isRemove:{}", remark, isRemove); + } else { + reason = remark + " Exception:" + throwable.getMessage(); + log.error("mqtt 链接断开 remark:{} isRemove:{}", remark, isRemove, throwable); + } + EventBus.publish(new MqttDisconnectEvent(reason, isRemove)); + } + +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/integration/MqttClientPluginImpl.java b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/integration/MqttClientPluginImpl.java new file mode 100644 index 0000000..6fc9cee --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/integration/MqttClientPluginImpl.java @@ -0,0 +1,173 @@ +/* Copyright (c) 2022 Peigen.info. All rights reserved. */ + +package org.dromara.mica.mqtt.client.solon.integration; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.annotation.MqttClientSubscribe; +import org.dromara.mica.mqtt.client.solon.MqttClientSubscribeListener; +import org.dromara.mica.mqtt.client.solon.MqttClientTemplate; +import org.dromara.mica.mqtt.client.solon.config.MqttClientConfiguration; +import org.dromara.mica.mqtt.client.solon.config.MqttClientProperties; +import org.dromara.mica.mqtt.core.client.*; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.noear.solon.Solon; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.BeanWrap; +import org.noear.solon.core.Plugin; +import org.noear.solon.core.util.ClassUtil; +import org.tio.core.ssl.SSLEngineCustomizer; +import org.tio.core.ssl.SslConfig; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** + * (MqttClientPluginImpl) + * + * @author Lihai、L.cm + * @version 1.0.0 + * @since 2023/7/20 + */ +@Slf4j +public class MqttClientPluginImpl implements Plugin { + private final List> subscribeClassTags = new ArrayList<>(); + private final List> subscribeMethodTags = new ArrayList<>(); + private AppContext context; + + @Override + public void start(AppContext context) throws Throwable { + this.context = context; //todo: 去掉 Solon.context() 写法,可同时兼容 2.5 之前与之后的版本 by noear,2023-09-15 + // 查找类上的 MqttClientSubscribe 注解 + context.beanBuilderAdd(MqttClientSubscribe.class, (clz, beanWrap, anno) -> { + subscribeClassTags.add(new ExtractorClassTag<>(clz, beanWrap, anno)); + }); + // 查找方法上的 MqttClientSubscribe 注解 + context.beanExtractorAdd(MqttClientSubscribe.class, (bw, method, anno) -> { + subscribeMethodTags.add(new ExtractorMethodTag<>(bw, method, anno)); + }); + context.lifecycle(-9, () -> { + context.beanMake(MqttClientProperties.class); + context.beanMake(MqttClientConfiguration.class); + MqttClientProperties properties = context.getBean(MqttClientProperties.class); + MqttClientCreator clientCreator = context.getBean(MqttClientCreator.class); + + // MqttClientTemplate init + IMqttClientConnectListener clientConnectListener = context.getBean(IMqttClientConnectListener.class); + MqttClientCustomizer customizers = context.getBean(MqttClientCustomizer.class); + MqttClientTemplate clientTemplate = new MqttClientTemplate(clientCreator, clientConnectListener, customizers); + BeanWrap mqttClientTemplateWrap = context.wrap(MqttClientTemplate.class, clientTemplate); + context.putWrap(MqttClientSubscribe.DEFAULT_CLIENT_TEMPLATE_BEAN, mqttClientTemplateWrap); + context.putWrap(MqttClientTemplate.class, mqttClientTemplateWrap); + + // ssl 自定义配置 + SslConfig sslConfig = clientCreator.getSslConfig(); + if (sslConfig != null) { + SSLEngineCustomizer sslCustomizer = context.getBean(SSLEngineCustomizer.class); + if (sslCustomizer != null) { + sslConfig.setSslEngineCustomizer(sslCustomizer); + } + } + + // 客户端 session + IMqttClientSession clientSession = context.getBean(IMqttClientSession.class); + clientCreator.clientSession(clientSession); + + // 添加启动时的临时订阅 + subscribeDetector(); + + // connect + if (properties.isEnabled()) { + clientTemplate.connect(); + } + }); + } + + private void subscribeDetector() { + // 类级别的注解订阅 + subscribeClassTags.forEach(each -> { + MqttClientSubscribe anno = each.getAnno(); + MqttClientTemplate clientTemplate = getMqttClientTemplate(anno); + // 订阅的 topic 转换 + String[] topicFilters = getTopicFilters(anno.value()); + // 订阅 + IMqttClientMessageListener clientMessageListener = each.getBeanWrap().get(); + clientTemplate.addSubscriptionList(topicFilters, anno.qos(), clientMessageListener); + }); + // 方法级别的注解订阅 + subscribeMethodTags.forEach(each -> { + MqttClientSubscribe anno = each.getAnno(); + MqttClientTemplate clientTemplate = getMqttClientTemplate(anno); + // 订阅的 topic 转换 + String[] topicTemplates = anno.value(); + String[] topicFilters = getTopicFilters(topicTemplates); + // 自定义的反序列化,支持 solon bean 或者 无参构造器初始化 + Class deserialized = anno.deserialize(); + MqttDeserializer deserializer = getMqttDeserializer(deserialized); + // 构造监听器 + Object bean = each.getBw().get(); + Method method = each.getMethod(); + // 订阅 + MqttClientSubscribeListener listener = new MqttClientSubscribeListener(bean, method, topicTemplates, topicFilters, deserializer); + clientTemplate.addSubscriptionList(topicFilters, anno.qos(), listener); + }); + } + + @Override + public void stop() throws Throwable { + MqttClientTemplate clientTemplate = context.getBean(MqttClientTemplate.class); + clientTemplate.destroy(); + log.info("mqtt client stop..."); + } + + private MqttClientTemplate getMqttClientTemplate(MqttClientSubscribe anno) { + String beanName = anno.clientTemplateBean(); + // 添加对占位符的支持:gitee #ID7PF6 https://gitee.com/dromara/mica-mqtt/issues/ID7PF6 + String resolvedBeanName = Optional.ofNullable(Solon.cfg().getByTmpl(beanName)).orElse(beanName); + return context.getBean(resolvedBeanName); + } + + /** + * 获取解码器 + * + * @param deserializerType deserializerType + * @return 解码器 + */ + private MqttDeserializer getMqttDeserializer(Class deserializerType) { + BeanWrap beanWrap = context.getWrap(deserializerType); + if (beanWrap == null) { + return ClassUtil.newInstance(deserializerType); + } + return beanWrap.get(); + } + + private String[] getTopicFilters(String[] topicTemplates) { + // 1. 替换 solon cfg 变量 + // 2. 替换订阅中的其他变量 + return Arrays.stream(topicTemplates) + .map((x) -> Optional.ofNullable(Solon.cfg().getByTmpl(x)).orElse(x)) + .map(TopicUtil::getTopicFilter) + .toArray(String[]::new); + } + + @Data + @RequiredArgsConstructor + private static class ExtractorClassTag { + private final Class clz; + private final BeanWrap beanWrap; + private final T anno; + } + + @Data + @RequiredArgsConstructor + private static class ExtractorMethodTag { + private final BeanWrap bw; + private final Method method; + private final T anno; + } +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/moditect/module-info.java b/starter/mica-mqtt-client-solon-plugin/src/main/moditect/module-info.java new file mode 100644 index 0000000..086ec6d --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/moditect/module-info.java @@ -0,0 +1,9 @@ +open module org.dromara.mica.mqtt.client.solon.plugin { + requires solon; + requires lombok; + requires transitive org.dromara.mica.mqtt.client; + exports org.dromara.mica.mqtt.client.solon; + exports org.dromara.mica.mqtt.client.solon.event; + exports org.dromara.mica.mqtt.client.solon.config; + provides org.noear.solon.core.Plugin with org.dromara.mica.mqtt.client.solon.integration.MqttClientPluginImpl; +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/resources/META-INF/solon/solon-configuration-metadata.json b/starter/mica-mqtt-client-solon-plugin/src/main/resources/META-INF/solon/solon-configuration-metadata.json new file mode 100644 index 0000000..2aa56ef --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/resources/META-INF/solon/solon-configuration-metadata.json @@ -0,0 +1,199 @@ +{ + "properties": [ + { + "name": "mqtt.client.bind-ip", + "type": "java.lang.String", + "description": "绑定 ip,绑定网卡,用于多网卡,默认为 null" + }, + { + "name": "mqtt.client.bind-network-interface", + "type": "java.lang.String", + "description": "绑定网卡,网卡名称,和 bindIp 取其一" + }, + { + "name": "mqtt.client.biz-thread-pool-size", + "type": "java.lang.Integer", + "description": "mqtt 工作线程数,默认:2,如果消息量比较大,处理较慢,例如做 emqx 的转发消息处理,可以调大此参数" + }, + { + "name": "mqtt.client.clean-start", + "type": "java.lang.Boolean", + "description": "清除会话

false 表示如果订阅的客户机断线了,那么要保存其要推送的消息,如果其重新连接时,则将这些消息推送。 true 表示消除,表示客户机是第一次连接,消息所以以前的连接信息。 <\/p>", + "defaultValue": true + }, + { + "name": "mqtt.client.client-id", + "type": "java.lang.String", + "description": "客户端ID" + }, + { + "name": "mqtt.client.debug", + "type": "java.lang.Boolean", + "description": "debug", + "defaultValue": false + }, + { + "name": "mqtt.client.enabled", + "type": "java.lang.Boolean", + "description": "是否启用,默认:true", + "defaultValue": true + }, + { + "name": "mqtt.client.global-subscribe", + "type": "java.util.List", + "description": "全局订阅" + }, + { + "name": "mqtt.client.heartbeat-mode", + "type": "org.tio.core.task.HeartbeatMode", + "description": "心跳模式,支持最后发送或接收心跳时间来计算心跳,默认:最后发送心跳的时间" + }, + { + "name": "mqtt.client.heartbeat-timeout-strategy", + "type": "org.tio.client.task.HeartbeatTimeoutStrategy", + "description": "心跳超时策略,支持发送 PING 和 CLOSE 断开连接,默认:最大努力发送 PING" + }, + { + "name": "mqtt.client.ip", + "type": "java.lang.String", + "description": "服务端 ip,默认:127.0.0.1", + "defaultValue": "127.0.0.1" + }, + { + "name": "mqtt.client.keep-alive-secs", + "type": "java.lang.Integer", + "description": "Keep Alive (s)", + "defaultValue": 60 + }, + { + "name": "mqtt.client.max-bytes-in-message", + "type": "org.springframework.util.unit.DataSize", + "description": "消息解析最大 bytes 长度,默认:10M" + }, + { + "name": "mqtt.client.max-client-id-length", + "type": "java.lang.Integer", + "description": "mqtt 3.1 会校验此参数为 23,为了减少问题设置成了 64" + }, + { + "name": "mqtt.client.name", + "type": "java.lang.String", + "description": "名称,默认:Mica-Mqtt-Client", + "defaultValue": "Mica-Mqtt-Client" + }, + { + "name": "mqtt.client.password", + "type": "java.lang.String", + "description": "密码" + }, + { + "name": "mqtt.client.port", + "type": "java.lang.Integer", + "description": "端口,默认:1883", + "defaultValue": 1883 + }, + { + "name": "mqtt.client.re-interval", + "type": "java.lang.Long", + "description": "重连的间隔时间,单位毫秒,默认:5000", + "defaultValue": 5000 + }, + { + "name": "mqtt.client.re-subscribe-batch-size", + "type": "java.lang.Integer", + "description": "重连,重新订阅一个批次大小,默认:20", + "defaultValue": 20 + }, + { + "name": "mqtt.client.read-buffer-size", + "type": "org.springframework.util.unit.DataSize", + "description": "接收数据的 buffer size,默认:8k" + }, + { + "name": "mqtt.client.reconnect", + "type": "java.lang.Boolean", + "description": "自动重连", + "defaultValue": true + }, + { + "name": "mqtt.client.retry-count", + "type": "java.lang.Integer", + "description": "连续重连次数,当连续重连这么多次都失败时,不再重连。0和负数则一直重连", + "defaultValue": 0 + }, + { + "name": "mqtt.client.session-expiry-interval-secs", + "type": "java.lang.Integer", + "description": "开启保留 session 时,session 的有效期,默认:0", + "defaultValue": 0 + }, + { + "name": "mqtt.client.ssl.enabled", + "type": "java.lang.Boolean", + "description": "启用 ssl", + "defaultValue": false + }, + { + "name": "mqtt.client.ssl.keystore-pass", + "type": "java.lang.String", + "description": "keystore 密码" + }, + { + "name": "mqtt.client.ssl.keystore-path", + "type": "java.lang.String", + "description": "keystore 证书路径" + }, + { + "name": "mqtt.client.ssl.truststore-pass", + "type": "java.lang.String", + "description": "truststore 密码" + }, + { + "name": "mqtt.client.ssl.truststore-path", + "type": "java.lang.String", + "description": "truststore 证书路径" + }, + { + "name": "mqtt.client.stat-enable", + "type": "java.lang.Boolean", + "description": "是否开启监控,默认:false 不开启,节省内存", + "defaultValue": false + }, + { + "name": "mqtt.client.timeout", + "type": "java.lang.Integer", + "description": "超时时间,单位:秒,t-io 配置,可为 null" + }, + { + "name": "mqtt.client.username", + "type": "java.lang.String", + "description": "用户名" + }, + { + "name": "mqtt.client.version", + "type": "org.dromara.mica.mqtt.codec.MqttVersion", + "description": "mqtt 协议,默认:MQTT_5" + }, + { + "name": "mqtt.client.will-message.message", + "type": "java.lang.String", + "description": "遗嘱消息 payload" + }, + { + "name": "mqtt.client.will-message.qos", + "type": "org.dromara.mica.mqtt.codec.MqttQoS", + "description": "遗嘱消息 qos,默认: qos0" + }, + { + "name": "mqtt.client.will-message.retain", + "type": "java.lang.Boolean", + "description": "遗嘱消息保留标识符,默认: false", + "defaultValue": false + }, + { + "name": "mqtt.client.will-message.topic", + "type": "java.lang.String", + "description": "遗嘱消息 topic" + } + ] +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/main/resources/META-INF/solon/solon.mica.client.properties b/starter/mica-mqtt-client-solon-plugin/src/main/resources/META-INF/solon/solon.mica.client.properties new file mode 100644 index 0000000..eed9224 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/main/resources/META-INF/solon/solon.mica.client.properties @@ -0,0 +1,2 @@ +solon.plugin=org.dromara.mica.mqtt.client.solon.integration.MqttClientPluginImpl +solon.plugin.priority=1 diff --git a/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/ClientTest.java b/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/ClientTest.java new file mode 100644 index 0000000..75b8fe0 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/ClientTest.java @@ -0,0 +1,32 @@ +package org.dromara.mica.mqtt.client.solon.test; + +import org.dromara.mica.mqtt.client.solon.MqttClientTemplate; +import org.noear.solon.Solon; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; +import org.noear.solon.core.event.AppLoadEndEvent; +import org.noear.solon.core.event.EventListener; + +import java.nio.charset.StandardCharsets; + +/** + * (ClientTest) + * + * @author Peigen + * @version 1.0.0 + * @since 2023/7/15 + */ +@Component +public class ClientTest implements EventListener { + public static void main(String[] args) { + Solon.start(ClientTest.class, args); + } + + @Inject + MqttClientTemplate client; + + @Override + public void onEvent(AppLoadEndEvent event) throws Throwable { + client.publish("mica", "hello".getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientConnectListener.java b/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientConnectListener.java new file mode 100644 index 0000000..2d448d0 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientConnectListener.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.client.solon.test.listener; + +import org.dromara.mica.mqtt.client.solon.event.MqttConnectedEvent; +import org.dromara.mica.mqtt.core.client.MqttClientCreator; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; +import org.noear.solon.core.event.EventListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 客户端连接状态监听 + * + * @author L.cm + */ +@Component +public class MqttClientConnectListener implements EventListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientConnectListener.class); + + @Inject + private MqttClientCreator mqttClientCreator; + + @Override + public void onEvent(MqttConnectedEvent mqttConnectedEvent) throws Throwable { + logger.info("MqttConnectedEvent:{}", mqttConnectedEvent); + } +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientDisConnectListener.java b/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientDisConnectListener.java new file mode 100644 index 0000000..99c9af2 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientDisConnectListener.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.client.solon.test.listener; + +import org.dromara.mica.mqtt.client.solon.event.MqttDisconnectEvent; +import org.dromara.mica.mqtt.core.client.MqttClientCreator; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; +import org.noear.solon.core.event.EventListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 客户端连接状态监听 + * + * @author L.cm + */ +@Component +public class MqttClientDisConnectListener implements EventListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientDisConnectListener.class); + + @Inject + private MqttClientCreator mqttClientCreator; + + @Override + public void onEvent(MqttDisconnectEvent mqttDisconnectEvent) throws Throwable { + logger.info("MqttDisconnectEvent:{}", mqttDisconnectEvent); + // 在断线时更新 clientId、username、password + mqttClientCreator.clientId("newClient" + System.currentTimeMillis()) + .username("newUserName") + .password("newPassword"); + } +} diff --git a/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientMessageListener.java b/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientMessageListener.java new file mode 100644 index 0000000..d4abd32 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientMessageListener.java @@ -0,0 +1,26 @@ +package org.dromara.mica.mqtt.client.solon.test.listener; + +import org.dromara.mica.mqtt.core.annotation.MqttClientSubscribe; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.client.IMqttClientMessageListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +import java.nio.charset.StandardCharsets; + +/** + * 客户端消息监听的另一种方式 + * + * @author L.cm + */ +@MqttClientSubscribe("${topic1}") +public class MqttClientMessageListener implements IMqttClientMessageListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientMessageListener.class); + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + logger.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } +} + diff --git a/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientSubscribeListener.java b/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientSubscribeListener.java new file mode 100644 index 0000000..d9100f8 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientSubscribeListener.java @@ -0,0 +1,38 @@ +package org.dromara.mica.mqtt.client.solon.test.listener; + +import org.dromara.mica.mqtt.core.annotation.MqttClientSubscribe; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.noear.solon.annotation.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +/** + * 客户端消息监听 + * + * @author L.cm + */ +@Component +public class MqttClientSubscribeListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientSubscribeListener.class); + + @MqttClientSubscribe("/test/#") + public void subQos0(String topic, byte[] payload) { + logger.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + + @MqttClientSubscribe(value = "/qos1/#", qos = MqttQoS.QOS1) + public void subQos1(String topic, byte[] payload) { + logger.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + + @MqttClientSubscribe("/sys/${productKey}/${deviceName}/thing/sub/register") + public void thingSubRegister(String topic, byte[] payload) { + // 1.3.8 开始支持,@MqttClientSubscribe 注解支持 ${} 变量替换,会默认替换成 + + // 注意:mica-mqtt 会先从 Spring boot 配置中替换参数 ${},如果存在配置会优先被替换。 + logger.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + +} + diff --git a/starter/mica-mqtt-client-solon-plugin/src/test/resources/app.yml b/starter/mica-mqtt-client-solon-plugin/src/test/resources/app.yml new file mode 100644 index 0000000..f036536 --- /dev/null +++ b/starter/mica-mqtt-client-solon-plugin/src/test/resources/app.yml @@ -0,0 +1,27 @@ +server: + port: 30036 +# mqtt-client 配置 +mqtt: + client: + enabled: true # 是否开启客户端,默认:true + ip: 127.0.0.1 # 连接的服务端 ip ,默认:127.0.0.1 + port: 18889 # 端口:默认:1883 +# clientId: 000001 # 客户端Id(非常重要,一般为设备 sn,不可重复) +# username: mica # 认证的用户名 +# password: mica # 认证的密码 +# timeout: 5 # 超时时间,单位:秒,默认:5秒 +# reconnect: true # 是否重连,默认:true +# re-interval: 5000 # 重连时间,默认 5000 毫秒 +# version: mqtt_3_1_1 # mqtt 协议版本,可选 MQTT_3_1、mqtt_3_1_1、mqtt_5,默认:mqtt_3_1_1 +# read-buffer-size: 8KB # 接收数据的 buffer size,默认:8k +# max-bytes-in-message: 10MB # 消息解析最大 bytes 长度,默认:10M +# keep-alive-secs: 60 # keep-alive 时间,单位:秒 +# clean-session: true # mqtt clean session,默认:true +# ssl: +# enabled: false # 是否开启 ssl 认证,2.1.0 开始支持双向认证 +# keystore-path: # 可选参数:ssl 双向认证 keystore 目录,支持 classpath:/ 路径。 +# keystore-pass: # 可选参数:ssl 双向认证 keystore 密码 +# truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 +# truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 + +topic1: /test2/# diff --git a/starter/mica-mqtt-client-spring-boot-starter/README.md b/starter/mica-mqtt-client-spring-boot-starter/README.md new file mode 100644 index 0000000..20e87b9 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/README.md @@ -0,0 +1,269 @@ +# mica-mqtt-client-spring-boot-starter 使用文档 + +## 版本兼容 +| 要求 | Spring boot 版本 | +|-----|----------------| +| 最高 | 4.x | +| 最低 | 2.1.0.RELEASE | + +## 一、添加依赖 + +```xml + + org.dromara.mica-mqtt + mica-mqtt-client-spring-boot-starter + ${最新版本} + +``` + +## 二、mqtt 客户端 + +### 2.1 配置项示例 +```yaml +mqtt: + client: + enabled: true # 是否开启客户端,默认:true + ip: 127.0.0.1 # 连接的服务端 ip ,默认:127.0.0.1 + port: 1883 # 端口:默认:1883 + name: Mica-Mqtt-Client # 名称,默认:Mica-Mqtt-Client + client-id: 000001 # 客户端Id(非常重要,一般为设备 sn,不可重复) + username: mica # 认证的用户名,注意:2.5.x 开始将 user-name 改成了 username + password: 123456 # 认证的密码 + global-subscribe: # 全局订阅的 topic,可被全局监听到,保留 session 停机重启,依然可以接受到消息。(2.2.9开始支持) + timeout: 5 # 超时时间,单位:秒,默认:5秒 + reconnect: true # 是否重连,默认:true + re-interval: 5000 # 重连时间,默认 5000 毫秒 + version: mqtt_3_1_1 # mqtt 协议版本,可选 MQTT_3_1、mqtt_3_1_1、mqtt_5,默认:mqtt_3_1_1 + read-buffer-size: 8KB # 接收数据的 buffer size,默认:8k + max-bytes-in-message: 10MB # 消息解析最大 bytes 长度,默认:10M + keep-alive-secs: 60 # keep-alive 时间,单位:秒 + heartbeat-mode: LAST_REQ # 心跳模式,支持最后发送或接收心跳时间来计算心跳,默认:最后发送心跳的时间。(2.4.3 开始支持) + heartbeat-timeout-strategy: PING # 心跳超时策略,支持发送 PING 和 CLOSE 断开连接,默认:最大努力发送 PING。(2.4.3 开始支持) + clean-start: true # session 保留 2.5.x 使用 clean-start,老版本用 clean-session,默认:true + session-expiry-interval-secs: 0 # 开启保留 session 时,session 的有效期,默认:0(2.4.2 开始支持) + biz-thread-pool-size: 2 # mqtt 工作线程数,默认:2,如果消息量比较大,处理较慢,例如做 emqx 的转发消息处理,可以调大此参数(2.4.2 开始支持) + ssl: + enabled: false # 是否开启 ssl 认证,2.1.0 开始支持双向认证 + keystore-path: # 可选参数:ssl 双向认证 keystore 目录,支持 classpath:/ 路径。 + keystore-pass: # 可选参数:ssl 双向认证 keystore 密码 + truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 + truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 +``` + +注意:**ssl** 存在三种情况 + +| 服务端开启ssl | 客户端 | +| ---------------------------------------- | --------------------------------------------- | +| ClientAuth 为 NONE(不需要客户端验证) | 仅仅需要开启 ssl 即可不用配置证书 | +| ClientAuth 为 OPTIONAL(与客户端协商) | 需开启 ssl 并且配置 truststore 证书 | +| ClientAuth 为 REQUIRE (必须的客户端验证) | 需开启 ssl 并且配置 truststore、 keystore证书 | + + +### 2.2 可实现接口(注册成 Spring Bean 即可) + +| 接口 | 是否必须 | 说明 | +| --------------------------- |------|--------------------------------| +| IMqttClientConnectListener | 否 | 客户端连接成功监听 | +| IMqttClientGlobalMessageListener | 否 | 全局消息监听,可以监听到所有订阅消息。(2.2.9开始支持) | + +### 2.3 客户端上下线监听 +使用 Spring event 解耦客户端上下线监听,注意: `1.3.4` 开始支持。会跟自定义的 `IMqttClientConnectListener` 实现冲突,取一即可。 + +```java +/** + * 示例:客户端连接状态监听 + * + * @author L.cm + */ +@Service +public class MqttClientConnectListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientConnectListener.class); + + @Autowired + private MqttClientCreator mqttClientCreator; + + @EventListener + public void onConnected(MqttConnectedEvent event) { + logger.info("MqttConnectedEvent:{}", event); + } + + @EventListener + public void onDisconnect(MqttDisconnectEvent event) { + // 离线时更新重连时的密码,适用于类似阿里云 mqtt clientId 连接带时间戳的方式 + logger.info("MqttDisconnectEvent:{}", event); + // 在断线时更新 clientId、username、password + mqttClientCreator.clientId("newClient" + System.currentTimeMillis()) + .username("newUserName") + .password("newPassword"); + } + +} +``` + +### 2.4 自定义 java 配置(可选) + +```java +@Configuration(proxyBeanMethods = false) +public class MqttClientCustomizerConfiguration { + + @Bean + public MqttClientCustomizer mqttClientCustomizer() { + return new MqttClientCustomizer() { + @Override + public void customize(MqttClientCreator creator) { + // 此处可自定义配置 creator,会覆盖 yml 中的配置 + System.out.println("----------------MqttServerCustomizer-----------------"); + } + }; + } + +} +``` + +### 2.5 订阅示例 +```java +@Service +public class MqttClientSubscribeListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientSubscribeListener.class); + + @MqttClientSubscribe("/test/#") + public void subQos0(String topic, byte[] payload) { + logger.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + + @MqttClientSubscribe(value = "/qos1/#", qos = MqttQoS.QOS1) + public void subQos1(String topic, byte[] payload) { + logger.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + + @MqttClientSubscribe("/sys/${productKey}/${deviceName}/thing/sub/register") + public void thingSubRegister(String topic, byte[] payload) { + // 1.3.8 开始支持,@MqttClientSubscribe 注解支持 ${} 变量替换,会默认替换成 + + // 注意:mica-mqtt 会先从 Spring boot 配置中替换参数 ${},如果存在配置会优先被替换。 + logger.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + + @MqttClientSubscribe( + value = "/test/json", + deserialize = MqttJsonDeserializer.class // 2.4.5 开始支持 自定义序列化,默认 json 序列化 + ) + public void testJson(String topic, MqttPublishMessage message, TestJsonBean data) { + // 2.4.5 开始支持,支持 2 到 3 个参数,字段类型映射规则如下 + // String 字符串会默认映射到 topic, + // MqttPublishMessage 会默认映射到 原始的消息,可以拿到 mqtt5 的 props 参数 + // byte[] 会映射到 mqtt 消息内容 payload + // ByteBuffer 会映射到 mqtt 消息内容 payload + // 其他类型会走序列化,确保消息能够序列化,默认为 json 序列化 + logger.info("topic:{} json data:{}", topic, data); + } + +} +``` + +### 2.6 共享订阅 topic 说明 +mica-mqtt 支持两种**共享订阅**方式: + +1. 共享订阅:订阅前缀 `$queue/`,多个客户端订阅了 `$queue/topic`,发布者发布到 `topic`,则只有一个客户端会接收到消息。 +2. 分组订阅:订阅前缀 `$share//`,组客户端订阅了 `$share/group1/topic`、`$share/group2/topic`..,发布者发布到 `topic`,则消息会发布到每个 **group** 中,但是每个 **group** 中只有一个客户端会接收到消息。 + +**注意:** 如果发布的 `topic` 以 `/` 开头,例如:`/topic/test`,需要订阅 `$share/group1//topic/test`,另外 mica-mqtt 默认随机消息路由,共享订阅的多个客户端会随机收到消息。 + +### 2.7 MqttClientTemplate 使用示例 + +```java + +import org.dromara.mica.mqtt.spring.client.MqttClientTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; + +/** + * @author wsq + */ +@Service +public class MainService { + private static final Logger logger = LoggerFactory.getLogger(MainService.class); + @Autowired + private MqttClientTemplate client; + + public boolean publish() { + client.publish("/test/client", "mica最牛皮".getBytes(StandardCharsets.UTF_8)); + return true; + } + + public boolean sub() { + client.subQos0("/test/#", (context, topic, message, payload) -> { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + }); + return true; + } + +} +``` + +## 3. 多个 mqtt client 客户端 +### 3.1 自定义 MqttClientTemplate bean 2.2.11 开始已简化,老版本建议先升级。 +```java +@Configuration +public class OtherMqttClientConfiguration { + + @Bean("mqttClientTemplate1") + public MqttClientTemplate mqttClientTemplate1() { + MqttClientCreator mqttClientCreator1 = MqttClient.create() + .ip("mqtt.dreamlu.net") + .username("mica") + .password("mica"); + return new MqttClientTemplate(mqttClientCreator1); + } + +} +``` + +### 3.2 修改 starter 自带的 MqttClientTemplate Bean 引入 +由于现在加入了一个新的名为 `mqttClientTemplate1` MqttClientTemplate,老的 starter 内置的 MqttClientTemplate 引入也需要添加 bean name。 + +```java +@Autowired +@Qualifier(MqttClientTemplate.DEFAULT_CLIENT_TEMPLATE_BEAN) +private MqttClientTemplate mqttClientTemplate; +``` + +### 3.3 新加入的 mqttClientTemplate1 MqttClientTemplate bean 引入 +```java +@Autowired +@Qualifier("mqttClientTemplate1") +private MqttClientTemplate mqttClientTemplate; +``` + +### 3.4 新加入的 mqttClientTemplate1 注解订阅 +注意:由于 `@MqttClientSubscribe` clientTemplateBean 默认是 `MqttClientTemplate.DEFAULT_CLIENT_TEMPLATE_BEAN`,所以新增的 `mqttClientTemplate1` 注解订阅的时候也需要配置。 +```java +@MqttClientSubscribe( + value = "/#", + clientTemplateBean = "mqttClientTemplate1" +) +public void sub1(String topic, byte[] payload) { + logger.info("topic:{} payload:{}", topic, ByteBufferUtil.toString(payload)); +} +``` + +### 接口代理 + +```java + +@EnableMqttClients +public class MqttClientApplication { + // ... +} + +@MqttClient(clientBean = "mqttClientTemplate") +public interface HelloInterfaceB { + + @MqttClientPublish("/test/HelloInterfaceB") + void sayHello(@MqttPayload Object payload); + +} +``` diff --git a/starter/mica-mqtt-client-spring-boot-starter/pom.xml b/starter/mica-mqtt-client-spring-boot-starter/pom.xml new file mode 100644 index 0000000..9348c28 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + starter + ${revision} + + mica-mqtt-client-spring-boot-starter + ${project.artifactId} + https://mica-mqtt.dreamlu.net/guide/spring/client.html + + + + org.dromara.mica-mqtt + mica-mqtt-client + + + org.springframework.boot + spring-boot-autoconfigure + provided + + + org.projectlombok + lombok + provided + + + + net.dreamlu + mica-auto + provided + + + + diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientFactoryBean.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientFactoryBean.java new file mode 100644 index 0000000..f190fc1 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientFactoryBean.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client; + + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * @author ChangJin Wei (魏昌进) + */ +public class MqttClientFactoryBean implements FactoryBean, ApplicationContextAware { + + private final Class interfaceClass; + + private final String mqttClientTemplateBeanName; + + private ApplicationContext applicationContext; + + public MqttClientFactoryBean(Class interfaceClass, String mqttClientTemplateBeanName) { + this.interfaceClass = interfaceClass; + this.mqttClientTemplateBeanName = mqttClientTemplateBeanName; + } + + @Override + public T getObject() { + MqttClientTemplate mqttClientTemplate = + applicationContext.getBean(mqttClientTemplateBeanName, MqttClientTemplate.class); + return mqttClientTemplate.getInterface(interfaceClass); + } + + @Override + public Class getObjectType() { + return interfaceClass; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } +} \ No newline at end of file diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientRegistrar.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientRegistrar.java new file mode 100644 index 0000000..3b46c61 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientRegistrar.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client; + +import org.dromara.mica.mqtt.spring.client.annotation.EnableMqttClients; +import org.dromara.mica.mqtt.spring.client.annotation.MqttClient; +import org.dromara.mica.mqtt.spring.client.config.MqttClientConfiguration; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.ClassUtils; + +import java.util.Map; +import java.util.Set; + +/** + * @author ChangJin Wei (魏昌进) + */ +@AutoConfigureAfter(MqttClientConfiguration.class) +public class MqttClientRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + Map attrs = importingClassMetadata.getAnnotationAttributes(EnableMqttClients.class.getName()); + String[] basePackages = (String[]) attrs.get("basePackages"); + + if (basePackages == null || basePackages.length == 0) { + basePackages = new String[]{ClassUtils.getPackageName(importingClassMetadata.getClassName())}; + } + + for (String basePackage : basePackages) { + scanAndRegisterClients(basePackage, registry); + } + } + + private void scanAndRegisterClients(String basePackage, BeanDefinitionRegistry registry) { + ClassPathScanningCandidateComponentProvider scanner = getScanner(); + scanner.addIncludeFilter(new AnnotationTypeFilter(MqttClient.class)); + + Set candidates = scanner.findCandidateComponents(basePackage); + for (BeanDefinition candidate : candidates) { + try { + String className = candidate.getBeanClassName(); + Class interfaceClass = Class.forName(className); + + MqttClient mqttClientAnnotation = AnnotationUtils.findAnnotation(interfaceClass, MqttClient.class); + if (mqttClientAnnotation == null) { + continue; + } + + String mqttClientTemplateBeanName = mqttClientAnnotation.clientBean(); + + // 构造 FactoryBean,注入接口和客户端 Bean 名称 + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(MqttClientFactoryBean.class); + builder.addConstructorArgValue(interfaceClass); + builder.addConstructorArgValue(mqttClientTemplateBeanName); + + // 注册为 Spring Bean,使用接口类名作为 beanName + String beanName = ClassUtils.getShortNameAsProperty(interfaceClass); + registry.registerBeanDefinition(beanName, builder.getBeanDefinition()); + + } catch (ClassNotFoundException e) { + throw new RuntimeException("Failed to load MQTT client interface", e); + } + } + } + + private ClassPathScanningCandidateComponentProvider getScanner() { + return new ClassPathScanningCandidateComponentProvider(false) { + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + return true; // 允许接口作为候选组件 + } + }; + } +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeDetector.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeDetector.java new file mode 100644 index 0000000..02c6c2c --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeDetector.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.annotation.MqttClientSubscribe; +import org.dromara.mica.mqtt.core.client.IMqttClientMessageListener; +import org.dromara.mica.mqtt.core.client.IMqttClientSession; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; +import org.springframework.lang.NonNull; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Modifier; +import java.util.Arrays; + +/** + * MqttClient 订阅监听器 + * + * @author L.cm + * @author ChangJin Wei(魏昌进) + */ +@Slf4j +@RequiredArgsConstructor +public class MqttClientSubscribeDetector implements BeanPostProcessor { + private final ApplicationContext applicationContext; + + @Override + public Object postProcessAfterInitialization(@NonNull Object bean, String beanName) throws BeansException { + Class userClass = ClassUtils.getUserClass(bean); + if (bean instanceof IMqttClientMessageListener) { + // 1. 查找类上的 MqttClientSubscribe 注解 + processClassLevelSubscription(userClass, bean); + } else { + // 2. 查找方法上的 MqttClientSubscribe 注解 + processMethodLevelSubscriptions(userClass, bean); + } + return bean; + } + + /** + * 处理类上的 @MqttClientSubscribe 注解。 + * 为实现了 IMqttClientMessageListener 接口的类自动注册 MQTT 订阅。 + */ + protected void processClassLevelSubscription(Class userClass, Object bean) { + MqttClientSubscribe subscribe = AnnotationUtils.findAnnotation(userClass, MqttClientSubscribe.class); + if (subscribe != null) { + IMqttClientSession clientSession = getMqttClientSession(subscribe.clientTemplateBean()); + String[] topicFilters = getTopicFilters(subscribe.value()); + clientSession.addSubscriptionList(topicFilters, subscribe.qos(), (IMqttClientMessageListener) bean); + } + } + + /** + * 处理方法上的 @MqttClientSubscribe 注解。 + * 为符合签名的方法自动注册 MQTT 订阅监听器。 + */ + protected void processMethodLevelSubscriptions(Class userClass, Object bean) { + ReflectionUtils.doWithMethods(userClass, method -> { + MqttClientSubscribe subscribe = AnnotationUtils.findAnnotation(method, MqttClientSubscribe.class); + if (subscribe != null) { + // 1. 校验必须为 public 和非 static 的方法 + int modifiers = method.getModifiers(); + if (Modifier.isStatic(modifiers)) { + throw new IllegalArgumentException("@MqttClientSubscribe on method " + method + " must not static."); + } + if (!Modifier.isPublic(modifiers)) { + throw new IllegalArgumentException("@MqttClientSubscribe on method " + method + " must public."); + } + // 2. 校验 method 入参数支持 2 ~ 3 个 + int paramCount = method.getParameterCount(); + if (paramCount < 2 || paramCount > 3) { + throw new IllegalArgumentException("@MqttClientSubscribe on method " + method + " parameter count must 2 ~ 3."); + } + // 3. 订阅存储,保存后,连接成功后会自动订阅 + IMqttClientSession clientSession = getMqttClientSession(subscribe.clientTemplateBean()); + // 4. 订阅的 topic 转换 + String[] topicTemplates = subscribe.value(); + String[] topicFilters = getTopicFilters(topicTemplates); + // 5. 自定义的反序列化,支持 Spring bean 或者 无参构造器初始化 + Class deserialized = subscribe.deserialize(); + @SuppressWarnings("unchecked") + MqttDeserializer deserializer = getMqttDeserializer((Class) deserialized); + // 6. 构造监听器 + MqttClientSubscribeListener listener = new MqttClientSubscribeListener(bean, method, topicTemplates, topicFilters, deserializer); + // 7. mqtt 订阅 + clientSession.addSubscriptionList(topicFilters, subscribe.qos(), listener); + } + }, ReflectionUtils.USER_DECLARED_METHODS); + } + + /** + * 读取 IMqttClientSession + * + * @param beanName beanName + * @return IMqttClientSession + */ + protected IMqttClientSession getMqttClientSession(String beanName) { + // 添加对占位符的支持:gitee #ID7PF6 https://gitee.com/dromara/mica-mqtt/issues/ID7PF6 + String resolvedBeanName = applicationContext.getEnvironment().resolvePlaceholders(beanName); + return applicationContext.getBean(resolvedBeanName, MqttClientTemplate.class).getClientCreator().getClientSession(); + } + + /** + * 获取解码器 + * + * @param deserializerType deserializerType + * @return 解码器 + */ + protected MqttDeserializer getMqttDeserializer(Class deserializerType) { + return applicationContext.getBeanProvider(deserializerType) + .getIfAvailable(() -> BeanUtils.instantiateClass(deserializerType)); + } + + /** + * 转换 topic filter + * + * @param topicTemplates 订阅含有变量的 topicTemplate + * @return topic filter + */ + protected String[] getTopicFilters(String[] topicTemplates) { + // 1. 替换 Spring boot env 变量 + // 2. 替换订阅中的其他变量 + Environment environment = applicationContext.getEnvironment(); + return Arrays.stream(topicTemplates) + .map(environment::resolvePlaceholders) + .map(TopicUtil::getTopicFilter) + .toArray(String[]::new); + } + +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeLazyFilter.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeLazyFilter.java new file mode 100644 index 0000000..530cb38 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeLazyFilter.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client; + +import org.dromara.mica.mqtt.core.annotation.MqttClientSubscribe; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.LazyInitializationExcludeFilter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * mqtt 客户端订阅延迟加载排除 + * + * @author L.cm + */ +public class MqttClientSubscribeLazyFilter implements LazyInitializationExcludeFilter { + + @Override + public boolean isExcluded(String beanName, BeanDefinition beanDefinition, Class beanType) { + // 类上有注解的情况 + MqttClientSubscribe subscribe = AnnotationUtils.findAnnotation(beanType, MqttClientSubscribe.class); + if (subscribe != null) { + return true; + } + // 方法上的注解 + List methodList = new ArrayList<>(); + ReflectionUtils.doWithMethods(beanType, method -> { + MqttClientSubscribe clientSubscribe = AnnotationUtils.findAnnotation(method, MqttClientSubscribe.class); + if (clientSubscribe != null) { + methodList.add(method); + } + }, ReflectionUtils.USER_DECLARED_METHODS); + return !methodList.isEmpty(); + } + +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeListener.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeListener.java new file mode 100644 index 0000000..fe89f0b --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeListener.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.client.IMqttClientMessageListener; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.function.ParamValueFunction; +import org.dromara.mica.mqtt.core.util.MethodParamUtil; +import org.springframework.util.ReflectionUtils; +import org.tio.core.ChannelContext; + +import java.lang.reflect.Method; + +/** + * MqttClientSubscribe 注解订阅监听 + * + * @author L.cm + */ +@Slf4j +public class MqttClientSubscribeListener implements IMqttClientMessageListener { + private final Object bean; + private final Method method; + private final ParamValueFunction[] paramValueFunctions; + + public MqttClientSubscribeListener(Object bean, Method method, String[] topicTemplates, String[] topicFilters, MqttDeserializer deserializer) { + this.bean = bean; + this.method = method; + this.paramValueFunctions = MethodParamUtil.getParamValueFunctions(method, topicTemplates, topicFilters, deserializer); + } + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + // 获取方法参数 + Object[] args = getMethodParameters(context, topic, message, payload); + // 反射执行方法 + ReflectionUtils.invokeMethod(method, bean, args); + } + + /** + * 获取反射参数 + * + * @param context context + * @param topic topic + * @param message message + * @param payload payload + * @return Object array + */ + protected Object[] getMethodParameters(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + int length = paramValueFunctions.length; + Object[] parameters = new Object[length]; + for (int i = 0; i < length; i++) { + ParamValueFunction function = paramValueFunctions[i]; + parameters[i] = function.getValue(context, topic, message, payload); + } + return parameters; + } + +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientTemplate.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientTemplate.java new file mode 100644 index 0000000..ad6a6dc --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientTemplate.java @@ -0,0 +1,449 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client; + +import lombok.Getter; +import org.dromara.mica.mqtt.codec.message.builder.MqttPublishBuilder; +import org.dromara.mica.mqtt.codec.properties.MqttProperties; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.client.*; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.Ordered; +import org.tio.client.ClientChannelContext; +import org.tio.client.TioClient; +import org.tio.client.TioClientConfig; +import org.tio.utils.timer.TimerTask; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * mqtt client 模板 + * + * @author wsq(冷月宫主) + * @author ChangJin Wei (魏昌进) + */ +public class MqttClientTemplate implements ApplicationContextAware, SmartInitializingSingleton, InitializingBean, DisposableBean, Ordered, IMqttClient { + public static final String DEFAULT_CLIENT_TEMPLATE_BEAN = "mqttClientTemplate"; + @Getter + private final MqttClientCreator clientCreator; + private ApplicationContext applicationContext; + private MqttClient client; + + public MqttClientTemplate(MqttClientCreator clientCreator) { + this.clientCreator = clientCreator; + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subQos0(String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS0, listener); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subQos1(String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS1, listener); + } + + /** + * 订阅 + * + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subQos2(String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, MqttQoS.QOS2, listener); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subscribe(MqttQoS mqttQoS, String topicFilter, IMqttClientMessageListener listener) { + return client.subscribe(mqttQoS, topicFilter, listener); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subscribe(String topicFilter, MqttQoS mqttQoS, IMqttClientMessageListener listener) { + return client.subscribe(topicFilter, mqttQoS, listener); + } + + /** + * 订阅 + * + * @param mqttQoS MqttQoS + * @param topicFilter topicFilter + * @param listener MqttMessageListener + * @param properties MqttProperties + * @return MqttClient + */ + public MqttClient subscribe(String topicFilter, MqttQoS mqttQoS, IMqttClientMessageListener listener, MqttProperties properties) { + return client.subscribe(topicFilter, mqttQoS, listener, properties); + } + + /** + * 订阅 + * + * @param topicFilters topicFilter 数组 + * @param mqttQoS MqttQoS + * @param listener MqttMessageListener + * @return MqttClient + */ + public MqttClient subscribe(String[] topicFilters, MqttQoS mqttQoS, IMqttClientMessageListener listener) { + return client.subscribe(topicFilters, mqttQoS, listener); + } + + /** + * 订阅 + * + * @param topicFilters topicFilter 数组 + * @param mqttQoS MqttQoS + * @param listener MqttMessageListener + * @param properties MqttProperties + * @return MqttClient + */ + public MqttClient subscribe(String[] topicFilters, MqttQoS mqttQoS, IMqttClientMessageListener listener, MqttProperties properties) { + return client.subscribe(topicFilters, mqttQoS, listener, properties); + } + + /** + * 批量订阅 + * + * @param subscriptionList 订阅集合 + * @return MqttClient + */ + public MqttClient subscribe(List subscriptionList) { + return client.subscribe(subscriptionList); + } + + /** + * 批量订阅 + * + * @param subscriptionList 订阅集合 + * @param properties MqttProperties + * @return MqttClient + */ + public MqttClient subscribe(List subscriptionList, MqttProperties properties) { + return client.subscribe(subscriptionList, properties); + } + + /** + * 取消订阅 + * + * @param topicFilters topicFilter 集合 + * @return MqttClient + */ + public MqttClient unSubscribe(String... topicFilters) { + return client.unSubscribe(topicFilters); + } + + /** + * 取消订阅 + * + * @param topicFilters topicFilter 集合 + * @return MqttClient + */ + public MqttClient unSubscribe(List topicFilters) { + return client.unSubscribe(topicFilters); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload) { + return client.publish(topic, payload, MqttQoS.QOS0); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos) { + return client.publish(topic, payload, qos); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息内容 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, boolean retain) { + return client.publish(topic, payload, retain); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos, boolean retain) { + return client.publish(topic, payload, qos, retain); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @param properties MqttProperties + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos, boolean retain, MqttProperties properties) { + return client.publish(topic, payload, qos, retain, properties); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param builder PublishBuilder + * @return 是否发送成功 + */ + public boolean publish(String topic, Object payload, MqttQoS qos, Consumer builder) { + return client.publish(topic, payload, qos, builder); + } + + /** + * 发布消息 + * + * @param builder PublishBuilder + * @return 是否发送成功 + */ + public boolean publish(MqttPublishBuilder builder) { + return client.publish(builder); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay) { + return client.schedule(command, delay); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay, Executor executor) { + return client.schedule(command, delay, executor); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay) { + return client.scheduleOnce(command, delay); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay, Executor executor) { + return client.scheduleOnce(command, delay, executor); + } + + /** + * 重连 + */ + public void reconnect() { + client.reconnect(); + } + + /** + * 重连到新的服务端节点 + * + * @param ip ip + * @param port port + * @return 是否成功 + */ + public boolean reconnect(String ip, int port) { + return client.reconnect(ip, port); + } + + /** + * 断开 mqtt 连接 + * + * @return 是否成功 + */ + public boolean disconnect() { + return client.disconnect(); + } + + /** + * 获取 TioClient + * + * @return TioClient + */ + public TioClient getTioClient() { + return client.getTioClient(); + } + + /** + * 获取 ClientTioConfig + * + * @return ClientTioConfig + */ + public TioClientConfig getClientTioConfig() { + return client.getClientTioConfig(); + } + + /** + * 获取 ClientChannelContext + * + * @return ClientChannelContext + */ + public ClientChannelContext getContext() { + return client.getContext(); + } + + /** + * 判断客户端跟服务端是否连接 + * + * @return 是否已经连接成功 + */ + public boolean isConnected() { + return client.isConnected(); + } + + /** + * 判断客户端跟服务端是否断开连接 + * + * @return 是否断连 + */ + public boolean isDisconnected() { + return client.isDisconnected(); + } + + /** + * 获取 MqttClient + * + * @return MqttClient + */ + @Override + public MqttClient getMqttClient() { + return client; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() throws Exception { + // 需要支持注解订阅所以 clientSession 配置的时机要早一些 + IMqttClientSession clientSession = this.clientCreator.getClientSession(); + if (clientSession == null) { + clientSession = this.applicationContext.getBeanProvider(IMqttClientSession.class) + .getIfAvailable(DefaultMqttClientSession::new); + this.clientCreator.clientSession(clientSession); + } + } + + @Override + public void afterSingletonsInstantiated() { + // 配置客户端连接监听器 + this.applicationContext.getBeanProvider(IMqttClientConnectListener.class).ifAvailable(clientCreator::connectListener); + // 全局监听器 + this.applicationContext.getBeanProvider(IMqttClientGlobalMessageListener.class).ifAvailable(clientCreator::globalMessageListener); + // 自定义处理 + this.applicationContext.getBeanProvider(MqttClientCustomizer.class).ifAvailable(customizer -> customizer.customize(clientCreator)); + // 连接超时时间,如果没设置,改成 3s,减少因连不上卡顿时间 + Integer timeout = clientCreator.getTimeout(); + if (timeout == null) { + clientCreator.timeout(3); + } + // 使用同步连接,不过如果连不上会卡一会 + client = clientCreator.connectSync(); + } + + @Override + public void destroy() { + client.stop(); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/annotation/EnableMqttClients.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/annotation/EnableMqttClients.java new file mode 100644 index 0000000..292c791 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/annotation/EnableMqttClients.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client.annotation; + +import org.dromara.mica.mqtt.spring.client.MqttClientRegistrar; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author ChangJin Wei (魏昌进) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Import(MqttClientRegistrar.class) +public @interface EnableMqttClients { + + String[] basePackages() default {}; +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/annotation/MqttClient.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/annotation/MqttClient.java new file mode 100644 index 0000000..7f66654 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/annotation/MqttClient.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client.annotation; + +import org.dromara.mica.mqtt.spring.client.MqttClientTemplate; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author ChangJin Wei (魏昌进) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +public @interface MqttClient { + + /** + * 指定要使用的 MqttClientTemplate Bean + */ + String clientBean() default MqttClientTemplate.DEFAULT_CLIENT_TEMPLATE_BEAN; +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/config/MqttClientConfiguration.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/config/MqttClientConfiguration.java new file mode 100644 index 0000000..1dd07c9 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/config/MqttClientConfiguration.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client.config; + +import org.dromara.mica.mqtt.codec.message.builder.MqttTopicSubscription; +import org.dromara.mica.mqtt.core.client.IMqttClientConnectListener; +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.dromara.mica.mqtt.core.client.MqttClientCreator; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.deserialize.MqttJsonDeserializer; +import org.dromara.mica.mqtt.spring.client.MqttClientSubscribeDetector; +import org.dromara.mica.mqtt.spring.client.MqttClientSubscribeLazyFilter; +import org.dromara.mica.mqtt.spring.client.MqttClientTemplate; +import org.dromara.mica.mqtt.spring.client.event.SpringEventMqttClientConnectListener; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; +import org.tio.core.ssl.SSLEngineCustomizer; +import org.tio.core.ssl.SslConfig; + +import java.util.List; + +/** + * mqtt client 配置 + * + * @author L.cm + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty( + prefix = MqttClientProperties.PREFIX, + name = "enabled", + havingValue = "true", + matchIfMissing = true +) +@EnableConfigurationProperties(MqttClientProperties.class) +public class MqttClientConfiguration { + + @Bean + @ConditionalOnMissingBean + public MqttDeserializer mqttDeserializer() { + return new MqttJsonDeserializer(); + } + + @Bean + @ConditionalOnMissingBean + public IMqttClientConnectListener springEventMqttClientConnectListener(ApplicationEventPublisher eventPublisher) { + return new SpringEventMqttClientConnectListener(eventPublisher); + } + + @Bean + public MqttClientCreator mqttClientCreator(MqttClientProperties properties, ObjectProvider sslCustomizers) { + MqttClientCreator clientCreator = MqttClient.create() + .name(properties.getName()) + .ip(properties.getIp()) + .port(properties.getPort()) + .username(properties.getUsername()) + .password(properties.getPassword()) + .clientId(properties.getClientId()) + .bindIp(properties.getBindIp()) + .bindNetworkInterface(properties.getBindNetworkInterface()) + .readBufferSize((int) properties.getReadBufferSize().toBytes()) + .maxBytesInMessage((int) properties.getMaxBytesInMessage().toBytes()) + .maxClientIdLength(properties.getMaxClientIdLength()) + .keepAliveSecs(properties.getKeepAliveSecs()) + .heartbeatMode(properties.getHeartbeatMode()) + .heartbeatTimeoutStrategy(properties.getHeartbeatTimeoutStrategy()) + .reconnect(properties.isReconnect()) + .reInterval(properties.getReInterval()) + .retryCount(properties.getRetryCount()) + .reSubscribeBatchSize(properties.getReSubscribeBatchSize()) + .version(properties.getVersion()) + .cleanStart(properties.isCleanStart()) + .sessionExpiryIntervalSecs(properties.getSessionExpiryIntervalSecs()) + .statEnable(properties.isStatEnable()) + .debug(properties.isDebug()) + .disconnectBeforeStop(properties.isDisconnectBeforeStop()); + Integer timeout = properties.getTimeout(); + if (timeout != null && timeout > 0) { + clientCreator.timeout(timeout); + } + // mqtt 业务线程数 + Integer bizThreadPoolSize = properties.getBizThreadPoolSize(); + if (bizThreadPoolSize != null && bizThreadPoolSize > 0) { + clientCreator.bizThreadPoolSize(bizThreadPoolSize); + } + // 开启 ssl + MqttClientProperties.Ssl ssl = properties.getSsl(); + if (ssl.isEnabled()) { + SslConfig sslConfig = SslConfig.forClient(ssl.getKeystorePath(), ssl.getKeystorePass(), ssl.getTruststorePath(), ssl.getTruststorePass()); + clientCreator.sslConfig(sslConfig); + sslCustomizers.ifAvailable(sslConfig::setSslEngineCustomizer); + } + // 构造遗嘱消息 + MqttClientProperties.WillMessage willMessage = properties.getWillMessage(); + if (willMessage != null && StringUtils.hasText(willMessage.getTopic())) { + clientCreator.willMessage(builder -> { + builder.topic(willMessage.getTopic()) + .qos(willMessage.getQos()) + .retain(willMessage.isRetain()); + if (StringUtils.hasText(willMessage.getMessage())) { + builder.messageText(willMessage.getMessage()); + } + }); + } + // 全局订阅 + List globalSubscribe = properties.getGlobalSubscribe(); + if (globalSubscribe != null && !globalSubscribe.isEmpty()) { + clientCreator.globalSubscribe(globalSubscribe); + } + return clientCreator; + } + + @Bean(MqttClientTemplate.DEFAULT_CLIENT_TEMPLATE_BEAN) + @ConditionalOnMissingBean(name = MqttClientTemplate.DEFAULT_CLIENT_TEMPLATE_BEAN) + public MqttClientTemplate mqttClientTemplate(MqttClientCreator mqttClientCreator) { + return new MqttClientTemplate(mqttClientCreator); + } + + @Bean + @ConditionalOnMissingBean + public static MqttClientSubscribeDetector mqttClientSubscribeDetector(ApplicationContext applicationContext) { + return new MqttClientSubscribeDetector(applicationContext); + } + + @Bean + public MqttClientSubscribeLazyFilter mqttClientSubscribeLazyFilter() { + return new MqttClientSubscribeLazyFilter(); + } + +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/config/MqttClientProperties.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/config/MqttClientProperties.java new file mode 100644 index 0000000..802328f --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/config/MqttClientProperties.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client.config; + +import lombok.Getter; +import lombok.Setter; +import org.dromara.mica.mqtt.codec.MqttConstant; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.builder.MqttTopicSubscription; +import org.dromara.mica.mqtt.codec.MqttVersion; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.unit.DataSize; +import org.tio.client.task.HeartbeatTimeoutStrategy; +import org.tio.core.task.HeartbeatMode; + +import java.util.List; + +/** + * MqttClient 配置 + * + * @author wsq(冷月宫主) + */ +@Getter +@Setter +@ConfigurationProperties(MqttClientProperties.PREFIX) +public class MqttClientProperties { + + /** + * 配置前缀 + */ + public static final String PREFIX = "mqtt.client"; + /** + * 是否启用,默认:true + */ + private boolean enabled = true; + /** + * 名称,默认:Mica-Mqtt-Client + */ + private String name = "Mica-Mqtt-Client"; + /** + * 服务端 ip,默认:127.0.0.1 + */ + private String ip = "127.0.0.1"; + /** + * 端口,默认:1883 + */ + private int port = 1883; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * 客户端ID + */ + private String clientId; + /** + * 超时时间,单位:秒,t-io 配置,可为 null + */ + private Integer timeout; + /** + * 绑定 ip,绑定网卡,用于多网卡,默认为 null + */ + private String bindIp; + /** + * 绑定网卡,网卡名称,和 bindIp 取其一 + */ + private String bindNetworkInterface; + /** + * 接收数据的 buffer size,默认:8k + */ + private DataSize readBufferSize = DataSize.ofBytes(MqttConstant.DEFAULT_MAX_READ_BUFFER_SIZE); + /** + * 消息解析最大 bytes 长度,默认:10M + */ + private DataSize maxBytesInMessage = DataSize.ofBytes(MqttConstant.DEFAULT_MAX_BYTES_IN_MESSAGE); + /** + * mqtt 3.1 会校验此参数为 23,为了减少问题设置成了 64 + */ + private int maxClientIdLength = MqttConstant.DEFAULT_MAX_CLIENT_ID_LENGTH; + /** + * Keep Alive (s) + */ + private int keepAliveSecs = 60; + /** + * 心跳模式,支持最后发送或接收心跳时间来计算心跳,默认:最后发送心跳的时间 + */ + private HeartbeatMode heartbeatMode = HeartbeatMode.LAST_REQ; + /** + * 心跳超时策略,支持发送 PING 和 CLOSE 断开连接,默认:最大努力发送 PING + */ + private HeartbeatTimeoutStrategy heartbeatTimeoutStrategy = HeartbeatTimeoutStrategy.PING; + /** + * 自动重连 + */ + private boolean reconnect = true; + /** + * 重连的间隔时间,单位毫秒,默认:5000 + */ + private long reInterval = 5000; + /** + * 连续重连次数,当连续重连这么多次都失败时,不再重连。0和负数则一直重连 + */ + private int retryCount = 0; + /** + * 重连,重新订阅一个批次大小,默认:20 + */ + private int reSubscribeBatchSize = 20; + /** + * mqtt 协议,默认:MQTT_5 + */ + private MqttVersion version = MqttVersion.MQTT_5; + /** + * 清除会话 + *

+ * false 表示如果订阅的客户机断线了,那么要保存其要推送的消息,如果其重新连接时,则将这些消息推送。 + * true 表示消除,表示客户机是第一次连接,消息所以以前的连接信息。 + *

+ */ + private boolean cleanStart = true; + /** + * 开启保留 session 时,session 的有效期,默认:0 + */ + private int sessionExpiryIntervalSecs = 0; + /** + * 遗嘱消息 + */ + private WillMessage willMessage; + /** + * 全局订阅 + */ + private List globalSubscribe; + /** + * 是否开启监控,默认:false 不开启,节省内存 + */ + private boolean statEnable = false; + /** + * debug + */ + private boolean debug = false; + /** + * mqtt 工作线程数,默认:2,如果消息量比较大,处理较慢,例如做 emqx 的转发消息处理,可以调大此参数 + */ + private Integer bizThreadPoolSize; + /** + * 停止前是否发送 disconnect 消息,默认:true 不会触发遗嘱消息 + */ + private boolean disconnectBeforeStop = true; + /** + * ssl 配置 + */ + private Ssl ssl = new Ssl(); + + @Getter + @Setter + public static class WillMessage { + /** + * 遗嘱消息 topic + */ + private String topic; + /** + * 遗嘱消息 qos,默认: qos0 + */ + private MqttQoS qos = MqttQoS.QOS0; + /** + * 遗嘱消息 payload + */ + private String message; + /** + * 遗嘱消息保留标识符,默认: false + */ + private boolean retain = false; + } + + @Getter + @Setter + public static class Ssl { + /** + * 启用 ssl + */ + private boolean enabled = false; + /** + * keystore 证书路径 + */ + private String keystorePath; + /** + * keystore 密码 + */ + private String keystorePass; + /** + * truststore 证书路径 + */ + private String truststorePath; + /** + * truststore 密码 + */ + private String truststorePass; + } + +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/MqttConnectedEvent.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/MqttConnectedEvent.java new file mode 100644 index 0000000..da55444 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/MqttConnectedEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client.event; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.tio.core.ChannelContext; + +import java.io.Serializable; + +/** + * mqtt 客户端连接成功事件 + * + * @author L.cm + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MqttConnectedEvent implements Serializable { + + /** + * context + */ + private ChannelContext context; + /** + * 是否重连 + */ + private boolean isReconnect; + +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/MqttDisconnectEvent.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/MqttDisconnectEvent.java new file mode 100644 index 0000000..cb77d99 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/MqttDisconnectEvent.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client.event; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.tio.core.ChannelContext; + +import java.io.Serializable; + +/** + * mqtt 客户端断开连接事件 + * + * @author L.cm + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MqttDisconnectEvent implements Serializable { + + /** + * context + */ + private ChannelContext context; + /** + * 断开原因 + */ + private String reason; + /** + * 是否删除连接 + */ + private boolean isRemove; + +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/SpringEventMqttClientConnectListener.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/SpringEventMqttClientConnectListener.java new file mode 100644 index 0000000..5aa02eb --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/SpringEventMqttClientConnectListener.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.client.event; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.client.IMqttClientConnectListener; +import org.springframework.context.ApplicationEventPublisher; +import org.tio.core.ChannelContext; + +/** + * spring event mqtt client 连接监听 + * + * @author L.cm + */ +@Slf4j +@RequiredArgsConstructor +public class SpringEventMqttClientConnectListener implements IMqttClientConnectListener { + private final ApplicationEventPublisher eventPublisher; + + @Override + public void onConnected(ChannelContext context, boolean isReconnect) { + if (isReconnect) { + log.info("重连 mqtt 服务器重连成功..."); + } else { + log.info("连接 mqtt 服务器成功..."); + } + eventPublisher.publishEvent(new MqttConnectedEvent(context, isReconnect)); + } + + @Override + public void onDisconnect(ChannelContext context, Throwable throwable, String remark, boolean isRemove) { + String reason; + if (throwable == null) { + reason = remark; + log.info("mqtt 链接断开 remark:{} isRemove:{}", remark, isRemove); + } else { + reason = remark + " Exception:" + throwable.getMessage(); + log.error("mqtt 链接断开 remark:{} isRemove:{}", remark, isRemove, throwable); + } + eventPublisher.publishEvent(new MqttDisconnectEvent(context, reason, isRemove)); + } + +} diff --git a/starter/mica-mqtt-client-spring-boot-starter/src/main/moditect/module-info.java b/starter/mica-mqtt-client-spring-boot-starter/src/main/moditect/module-info.java new file mode 100644 index 0000000..ecad9c0 --- /dev/null +++ b/starter/mica-mqtt-client-spring-boot-starter/src/main/moditect/module-info.java @@ -0,0 +1,12 @@ +open module org.dromara.mica.mqtt.client.spring.boot.starter { + requires lombok; + requires spring.core; + requires spring.context; + requires spring.boot; + requires spring.boot.autoconfigure; + requires transitive org.dromara.mica.mqtt.client; + exports org.dromara.mica.mqtt.spring.client; + exports org.dromara.mica.mqtt.spring.client.annotation; + exports org.dromara.mica.mqtt.spring.client.config; + exports org.dromara.mica.mqtt.spring.client.event; +} diff --git a/starter/mica-mqtt-server-jfinal-plugin/README.md b/starter/mica-mqtt-server-jfinal-plugin/README.md new file mode 100644 index 0000000..5189614 --- /dev/null +++ b/starter/mica-mqtt-server-jfinal-plugin/README.md @@ -0,0 +1,43 @@ +# jfinal mica-mqtt-server + +## 使用 + +#### 1. 添加依赖 +```xml + + org.dromara.mica-mqtt + mica-mqtt-server-jfinal-plugin + ${最新版本} + +``` + +#### 2. 删除 jfinal-demo 中的 slf4j-nop 依赖 + +#### 3. 添加 slf4j-log4j12 +```xml + + org.slf4j + slf4j-log4j12 + 1.7.33 + +``` + +#### 4. 插件配置 +```java +MqttServerPlugin plugin = new MqttServerPlugin(); +plugin.config(mqttServerCreator -> { + // mqttServerCreator 上有很多方法,详见 mica-mqtt-core + mqttServerCreator + .enableMqtt() + .enableMqttWs() + .enableMqttHttpApi() + ; +}); +plugin.start(); +``` + +#### 5. 插件使用 +```java +// 更多方法可以直接使用 MqttServerKit 点出来 +MqttServerKit.publish(String clientId, String topic, byte[] payload); +``` \ No newline at end of file diff --git a/starter/mica-mqtt-server-jfinal-plugin/pom.xml b/starter/mica-mqtt-server-jfinal-plugin/pom.xml new file mode 100644 index 0000000..9d62450 --- /dev/null +++ b/starter/mica-mqtt-server-jfinal-plugin/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + starter + ${revision} + + mica-mqtt-server-jfinal-plugin + ${project.artifactId} + https://mica-mqtt.dreamlu.net/guide/jfinal/server.html + + + + org.dromara.mica-mqtt + mica-mqtt-server + + + com.jfinal + jfinal + provided + + + org.junit.jupiter + junit-jupiter-engine + test + + + + diff --git a/starter/mica-mqtt-server-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/server/MqttServerKit.java b/starter/mica-mqtt-server-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/server/MqttServerKit.java new file mode 100644 index 0000000..c5b941c --- /dev/null +++ b/starter/mica-mqtt-server-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/server/MqttServerKit.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.jfinal.server; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.tio.core.ChannelContext; + +/** + * mica mqtt server kit + * + * @author L.cm + * @author ChangJin Wei (魏昌进) + */ +public class MqttServerKit { + private static MqttServer mqttServer; + + /** + * 初始化 + * @param mqttServer MqttServer + */ + static void init(MqttServer mqttServer) { + MqttServerKit.mqttServer = mqttServer; + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @return 是否发送成功 + */ + public static boolean publish(String clientId, String topic, Object payload) { + return mqttServer.publish(clientId, topic, payload); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public static boolean publish(String clientId, String topic, Object payload, MqttQoS qos) { + return mqttServer.publish(clientId, topic, payload, qos); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public static boolean publish(String clientId, String topic, Object payload, boolean retain) { + return mqttServer.publish(clientId, topic, payload, retain); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public static boolean publish(String clientId, String topic, Object payload, MqttQoS qos, boolean retain) { + return mqttServer.publish(clientId, topic, payload, qos, retain); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @return 是否发送成功 + */ + public static boolean publishAll(String topic, Object payload) { + return mqttServer.publishAll(topic, payload, MqttQoS.QOS0, false); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public static boolean publishAll(String topic, Object payload, MqttQoS qos) { + return mqttServer.publishAll(topic, payload, qos, false); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public static boolean publishAll(String topic, Object payload, boolean retain) { + return mqttServer.publishAll(topic, payload, MqttQoS.QOS0, retain); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public static boolean publishAll(String topic, Object payload, MqttQoS qos, boolean retain) { + return mqttServer.publishAll(topic, payload, qos, retain); + } + + /** + * 获取 ChannelContext + * + * @param clientId clientId + * @return ChannelContext + */ + public static ChannelContext getChannelContext(String clientId) { + return mqttServer.getChannelContext(clientId); + } + + /** + * 服务端主动断开连接 + * + * @param clientId clientId + */ + public static void close(String clientId) { + mqttServer.close(clientId); + } + +} diff --git a/starter/mica-mqtt-server-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/server/MqttServerPlugin.java b/starter/mica-mqtt-server-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/server/MqttServerPlugin.java new file mode 100644 index 0000000..64bed2a --- /dev/null +++ b/starter/mica-mqtt-server-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/server/MqttServerPlugin.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.jfinal.server; + +import com.jfinal.plugin.IPlugin; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.core.server.MqttServerCreator; + +import java.util.function.Consumer; + +/** + * mica mqtt server 插件 + * + * @author L.cm + */ +public class MqttServerPlugin implements IPlugin { + private final MqttServerCreator serverCreator; + private MqttServer mqttServer; + + public MqttServerPlugin() { + this.serverCreator = new MqttServerCreator(); + } + + /** + * 配置 mica mqtt + * + * @param consumer MqttServerCreator Consumer + */ + public void config(Consumer consumer) { + consumer.accept(this.serverCreator); + } + + @Override + public boolean start() { + if (this.mqttServer == null) { + this.mqttServer = serverCreator.start(); + } + MqttServerKit.init(this.mqttServer); + return true; + } + + @Override + public boolean stop() { + if (this.mqttServer != null) { + this.mqttServer.stop(); + } + return true; + } + +} diff --git a/starter/mica-mqtt-server-jfinal-plugin/src/main/moditect/module-info.java b/starter/mica-mqtt-server-jfinal-plugin/src/main/moditect/module-info.java new file mode 100644 index 0000000..42299ec --- /dev/null +++ b/starter/mica-mqtt-server-jfinal-plugin/src/main/moditect/module-info.java @@ -0,0 +1,5 @@ +open module org.dromara.mica.mqtt.server.jfinal.plugin { + requires jfinal; + requires transitive org.dromara.mica.mqtt.server; + exports org.dromara.mica.mqtt.jfinal.server; +} diff --git a/starter/mica-mqtt-server-jfinal-plugin/src/test/java/org/dromara/mica/mqtt/jfinal/server/MqttServerPluginTest.java b/starter/mica-mqtt-server-jfinal-plugin/src/test/java/org/dromara/mica/mqtt/jfinal/server/MqttServerPluginTest.java new file mode 100644 index 0000000..dd4c9e5 --- /dev/null +++ b/starter/mica-mqtt-server-jfinal-plugin/src/test/java/org/dromara/mica/mqtt/jfinal/server/MqttServerPluginTest.java @@ -0,0 +1,23 @@ +package org.dromara.mica.mqtt.jfinal.server; + +/** + * mica mqtt server 插件测试 + * + * @author L.cm + */ +public class MqttServerPluginTest { + + public static void main(String[] args) { + MqttServerPlugin plugin = new MqttServerPlugin(); + plugin.config(mqttServerCreator -> { + // mqttServerCreator 上有很多方法,详见 mica-mqtt-core + mqttServerCreator + .enableMqtt() + .enableMqttWs() + .enableMqttHttpApi() + ; + }); + plugin.start(); + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/README.md b/starter/mica-mqtt-server-solon-plugin/README.md new file mode 100644 index 0000000..219e37d --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/README.md @@ -0,0 +1,199 @@ +# mica-mqtt-server-solon-plugin 使用文档 + +本插件基于 https://gitee.com/peigenlpy/mica-mqtt-solon-plugin 调整合并到官方(已经过作者同意)。 + +## 版本兼容 +| 要求 | Solon 版本 | +|-----|-----------| +| 最高 | 3.x | +| 最低 | 2.8.0 | + +## 一、添加依赖 + +```xml + + org.dromara.mica-mqtt + mica-mqtt-server-solon-plugin + ${version} + +``` + +## 二、mqtt 服务 + +### 2.1 配置项 + +```yaml +# mqtt 服务端配置 +mqtt: + server: + enabled: true # 是否开启服务端,默认:true + name: Mica-Mqtt-Server # 名称,默认:Mica-Mqtt-Server + heartbeat-timeout: 120000 # 心跳超时,单位毫秒,默认: 1000 * 120 + read-buffer-size: 8KB # 接收数据的 buffer size,默认:8k + max-bytes-in-message: 10MB # 消息解析最大 bytes 长度,默认:10M + auth: + enable: false # 是否开启 mqtt 认证 + username: mica # mqtt 认证用户名 + password: mica # mqtt 认证密码 + debug: true # 如果开启 prometheus 指标收集建议关闭 + stat-enable: true # 开启指标收集,debug 和 prometheus 开启时需要打开,默认开启,关闭节省内存 + mqtt-listener: # mqtt 监听器 + enable: true # 是否开启,默认:false +# ip: "0.0.0.0" # 服务端 ip 默认为空,0.0.0.0,建议不要设置 + port: 1883 # 端口,默认:1883 + mqtt-ssl-listener: # mqtt ssl 监听器 + enable: false # 是否开启,默认:false + port: 8883 # 端口,默认:8883 + ssl: # ssl 配置,必须 + keystore-path: # 必须参数:ssl keystore 目录,支持 classpath:/ 路径。 + keystore-pass: # 必选参数:ssl keystore 密码 + truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 + truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 + client-auth: none # 是否需要客户端认证(双向认证),默认:NONE(不需要) + ws-listener: # websocket mqtt 监听器 + enable: true # 是否开启,默认:false + port: 8083 # websocket 端口,默认:8083 + wss-listener: # websocket ssl mqtt 监听器 + enable: false # 是否开启,默认:false + port: 8084 # 端口,默认:8084 + ssl: # ssl 配置,必须 + keystore-path: # 必须参数:ssl keystore 目录,支持 classpath:/ 路径。 + keystore-pass: # 必选参数:ssl keystore 密码 + truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 + truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 + client-auth: none # 是否需要客户端认证(双向认证),默认:NONE(不需要) + http-listener: + enable: true + port: 18083 + basic-auth: # 基础认证 + enable: true + username: mica + password: mica + mcp-server: # 大模型 mcp + enable: true +``` + +注意:**ssl** 存在三种情况 + +| 服务端开启ssl | 客户端 | +| ---------------------------------------- | --------------------------------------------- | +| ClientAuth 为 NONE(不需要客户端验证) | 仅仅需要开启 ssl 即可不用配置证书 | +| ClientAuth 为 OPTIONAL(与客户端协商) | 需开启 ssl 并且配置 truststore 证书 | +| ClientAuth 为 REQUIRE (必须的客户端验证) | 需开启 ssl 并且配置 truststore、 keystore证书 | + +### 2.2 可实现接口(注册成 Solon Bean 即可) + +| 接口 | 是否必须 | 说明 | +|-------------------------------|------------|-----------------------------------------------| +| IMqttServerUniqueIdService | 否 | 用于 clientId 不唯一时,自定义实现唯一标识,后续接口使用它替代 clientId | +| IMqttServerAuthHandler | 是 | 用于服务端认证 | +| IMqttServerSubscribeValidator | 否(建议实现) | 1.1.3 新增,用于对客户端订阅校验 | +| IMqttServerPublishPermission | 否(建议实现) | 1.2.2 新增,用于对客户端发布权限校验 | +| IMqttMessageListener | 否(1.3.x为否) | 消息监听 | +| IMqttConnectStatusListener | 是 | 连接状态监听 | +| IMqttSessionManager | 否 | session 管理 | +| IMqttSessionListener | 否 | session 监听 | +| IMqttMessageStore | 集群是,单机否 | 遗嘱和保留消息存储 | +| AbstractMqttMessageDispatcher | 集群是,单机否 | 消息转发,(遗嘱、保留消息转发) | +| IpStatListener | 否 | t-io ip 状态监听 | +| IMqttMessageInterceptor | 否 | 消息拦截器,1.3.9 新增 | + +### 2.3 IMqttMessageListener (用于监听客户端上传的消息) 使用示例 + +```java +@Component +public class MqttServerMessageListener implements IMqttMessageListener { + private static final Logger logger = LoggerFactory.getLogger(MqttServerMessageListener.class); + + @Override + public void onMessage(ChannelContext context, String clientId, String topic, MqttQoS qoS, MqttPublishMessage message) { + log.info("clientId:{} message:{} payload:{}", clientId, message, new String(message.getPayload(), StandardCharsets.UTF_8)); + } +} +``` + +### 2.4 自定义配置(可选) + +```java +@Configuration +public class MqttServerCustomizerConfiguration { + + @Bean + public MqttServerCustomizer mqttServerCustomizer() { + return new MqttServerCustomizer() { + @Override + public void customize(MqttServerCreator creator) { + // 此处可自定义配置 creator,会覆盖 yml 中的配置 + System.out.println("----------------MqttServerCustomizer-----------------"); + } + }; + } + +} +``` + +### 2.5 MqttServerTemplate 使用示例 + +```java +@Component +public class ServerService { + @Inject + private MqttServerTemplate server; + + public boolean publish(String body) { + server.publishAll("/test/123", body.getBytes(StandardCharsets.UTF_8)); + return true; + } +} +``` + +### 2.6 客户端上下线监听 +使用 Solon event 解耦客户端上下线监听,注意:会跟自定义的 `IMqttConnectStatusListener` 实现冲突,取一即可。 + +```java +@Component +public class MqttConnectOfflineListener implements EventListener { + private static final Logger logger = LoggerFactory.getLogger(MqttConnectOfflineListener.class); + + @Override + public void onEvent(MqttClientOfflineEvent mqttClientOfflineEvent) throws Throwable { + logger.info("MqttClientOnlineEvent:{}", mqttClientOfflineEvent); + } +} +``` + +```java +@Component +public class MqttConnectOnlineListener implements EventListener { + private static final Logger logger = LoggerFactory.getLogger(MqttConnectOnlineListener.class); + + @Override + public void onEvent(MqttClientOnlineEvent mqttClientOnlineEvent) throws Throwable { + logger.info("MqttClientOnlineEvent:{}", mqttClientOnlineEvent); + } +} +``` + +### 2.7 Prometheus + Grafana 监控对接 +```xml + + org.noear + solon-cloud-metrics + + + io.micrometer + micrometer-registry-prometheus + +``` + +| 支持的指标 | 说明 | +|--------------------------------| ---------------- | +| mqtt_connections_accepted | 共接受过连接数 | +| mqtt_connections_closed | 关闭过的连接数 | +| mqtt_connections_size | 当前连接数 | +| mqtt_messages_handled_packets | 已处理消息数 | +| mqtt_messages_handled_bytes | 已处理消息字节数 | +| mqtt_messages_received_packets | 已接收消息数 | +| mqtt_messages_received_bytes | 已处理消息字节数 | +| mqtt_messages_send_packets | 已发送消息数 | +| mqtt_messages_send_bytes | 已发送消息字节数 | \ No newline at end of file diff --git a/starter/mica-mqtt-server-solon-plugin/pom.xml b/starter/mica-mqtt-server-solon-plugin/pom.xml new file mode 100644 index 0000000..700eeb2 --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + starter + ${revision} + + mica-mqtt-server-solon-plugin + ${project.artifactId} + https://mica-mqtt.dreamlu.net/guide/solon/server.html + + + + org.dromara.mica-mqtt + mica-mqtt-server + + + org.noear + solon + provided + + + + io.micrometer + micrometer-core + provided + + + + org.noear + solon-web + test + + + org.noear + solon-scheduling-simple + test + + + org.noear + solon-logging-simple + test + + + org.projectlombok + lombok + provided + + + + diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/MqttServerTemplate.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/MqttServerTemplate.java new file mode 100644 index 0000000..2d0e95a --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/MqttServerTemplate.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.server.solon; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.core.server.model.ClientInfo; +import org.dromara.mica.mqtt.core.server.model.Subscribe; +import org.tio.core.ChannelContext; +import org.tio.core.stat.vo.StatVo; +import org.tio.utils.page.Page; +import org.tio.utils.timer.TimerTask; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * mqtt Server 模板 + * + * @author wsq(冷月宫主) + * @author ChangJin Wei (魏昌进) + */ +@Getter +@RequiredArgsConstructor +public class MqttServerTemplate { + private final MqttServer mqttServer; + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload) { + return mqttServer.publish(clientId, topic, payload); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload, MqttQoS qos) { + return mqttServer.publish(clientId, topic, payload, qos); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload, boolean retain) { + return mqttServer.publish(clientId, topic, payload, retain); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload, MqttQoS qos, boolean retain) { + return mqttServer.publish(clientId, topic, payload, qos, retain); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload) { + return mqttServer.publishAll(topic, payload); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload, MqttQoS qos) { + return mqttServer.publishAll(topic, payload, qos); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload, boolean retain) { + return mqttServer.publishAll(topic, payload, retain); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload, MqttQoS qos, boolean retain) { + return mqttServer.publishAll(topic, payload, qos, retain); + } + + /** + * 获取客户端信息 + * + * @param clientId clientId + * @return ClientInfo + */ + public ClientInfo getClientInfo(String clientId) { + return mqttServer.getClientInfo(clientId); + } + + /** + * 获取客户端信息 + * + * @param context ChannelContext + * @return ClientInfo + */ + public ClientInfo getClientInfo(ChannelContext context) { + return mqttServer.getClientInfo(context); + } + + /** + * 获取所有的客户端 + * + * @return 客户端列表 + */ + public List getClients() { + return mqttServer.getClients(); + } + + /** + * 分页获取所有的客户端 + * + * @param pageIndex pageIndex,默认为 1 + * @param pageSize pageSize,默认为所有 + * @return 分页 + */ + public Page getClients(Integer pageIndex, Integer pageSize) { + return mqttServer.getClients(pageIndex, pageSize); + } + + /** + * 获取统计数据 + * @return StatVo + */ + public StatVo getStat() { + return mqttServer.getStat(); + } + + /** + * 获取客户端订阅情况 + * + * @param clientId clientId + * @return 订阅集合 + */ + public List getSubscriptions(String clientId) { + return mqttServer.getSubscriptions(clientId); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay) { + return mqttServer.schedule(command, delay); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay, Executor executor) { + return mqttServer.schedule(command, delay, executor); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay) { + return mqttServer.scheduleOnce(command, delay); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay, Executor executor) { + return mqttServer.scheduleOnce(command, delay, executor); + } + + /** + * 获取 ChannelContext + * + * @param clientId clientId + * @return ChannelContext + */ + public ChannelContext getChannelContext(String clientId) { + return mqttServer.getChannelContext(clientId); + } + + /** + * 服务端主动断开连接 + * + * @param clientId clientId + */ + public void close(String clientId) { + mqttServer.close(clientId); + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/DataSize.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/DataSize.java new file mode 100644 index 0000000..f444b63 --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/DataSize.java @@ -0,0 +1,195 @@ +package org.dromara.mica.mqtt.server.solon.config; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.tio.utils.hutool.StrUtil; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * DataSize 兼容 + * + * @author L.cm + */ +@Getter +@RequiredArgsConstructor +class DataSize { + + /** + * Bytes per Kilobyte. + */ + private static final long BYTES_PER_KB = 1024; + + /** + * Bytes per Megabyte. + */ + private static final long BYTES_PER_MB = BYTES_PER_KB * 1024; + + /** + * Bytes per Gigabyte. + */ + private static final long BYTES_PER_GB = BYTES_PER_MB * 1024; + + /** + * Bytes per Terabyte. + */ + private static final long BYTES_PER_TB = BYTES_PER_GB * 1024; + + private final long bytes; + + /** + * Obtain a {@link DataSize} representing the specified number of bytes. + * @param bytes the number of bytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofBytes(long bytes) { + return new DataSize(bytes); + } + + /** + * Obtain a {@link DataSize} representing the specified number of kilobytes. + * @param kilobytes the number of kilobytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofKilobytes(long kilobytes) { + return new DataSize(Math.multiplyExact(kilobytes, BYTES_PER_KB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of megabytes. + * @param megabytes the number of megabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofMegabytes(long megabytes) { + return new DataSize(Math.multiplyExact(megabytes, BYTES_PER_MB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of gigabytes. + * @param gigabytes the number of gigabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofGigabytes(long gigabytes) { + return new DataSize(Math.multiplyExact(gigabytes, BYTES_PER_GB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of terabytes. + * @param terabytes the number of terabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofTerabytes(long terabytes) { + return new DataSize(Math.multiplyExact(terabytes, BYTES_PER_TB)); + } + + /** + * Obtain a {@link DataSize} representing an amount in the specified {@link DataUnit}. + * @param amount the amount of the size, measured in terms of the unit, + * positive or negative + * @return a corresponding {@link DataSize} + */ + public static DataSize of(long amount, DataUnit unit) { + Objects.requireNonNull(unit, "Unit must not be null"); + return new DataSize(Math.multiplyExact(amount, unit.getSize().getBytes())); + } + + /** + * Obtain a {@link DataSize} from a text string such as {@code 12MB} using + * the specified default {@link DataUnit} if no unit is specified. + *

+ * The string starts with a number followed optionally by a unit matching one of the + * supported {@linkplain DataUnit suffixes}. + *

+ * Examples: + *

+	 * "12KB" -- parses as "12 kilobytes"
+	 * "5MB"  -- parses as "5 megabytes"
+	 * "20"   -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
+	 * 
+ * @param text the text to parse + * @return the parsed {@link DataSize} + */ + public static DataSize parse(String text) { + Objects.requireNonNull(text, "Text must not be null"); + try { + Matcher matcher = DataSizeUtils.PATTERN.matcher(text.trim()); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid data size: " + text); + } + DataUnit unit = DataSizeUtils.determineDataUnit(matcher.group(2)); + long amount = Long.parseLong(matcher.group(1)); + return DataSize.of(amount, unit); + } + catch (Exception ex) { + throw new IllegalArgumentException("'" + text + "' is not a valid data size", ex); + } + } + + /** + * Static nested class to support lazy loading of the {@link #PATTERN}. + * @since 5.3.21 + */ + private static class DataSizeUtils { + /** + * The pattern for parsing. + */ + private static final Pattern PATTERN = Pattern.compile("^([+\\-]?\\d+)([a-zA-Z]{0,2})$"); + + private static DataUnit determineDataUnit(String suffix) { + return (StrUtil.isNotBlank(suffix) ? DataUnit.fromSuffix(suffix) : DataUnit.BYTES); + } + } + + @Getter + @RequiredArgsConstructor + public enum DataUnit { + + /** + * Bytes, represented by suffix {@code B}. + */ + BYTES("B", DataSize.ofBytes(1)), + + /** + * Kilobytes, represented by suffix {@code KB}. + */ + KILOBYTES("KB", DataSize.ofKilobytes(1)), + + /** + * Megabytes, represented by suffix {@code MB}. + */ + MEGABYTES("MB", DataSize.ofMegabytes(1)), + + /** + * Gigabytes, represented by suffix {@code GB}. + */ + GIGABYTES("GB", DataSize.ofGigabytes(1)), + + /** + * Terabytes, represented by suffix {@code TB}. + */ + TERABYTES("TB", DataSize.ofTerabytes(1)); + + + private final String suffix; + private final DataSize size; + + /** + * Return the {@link DataUnit} matching the specified {@code suffix}. + * @param suffix one of the standard suffixes + * @return the {@link DataUnit} matching the specified {@code suffix} + * @throws IllegalArgumentException if the suffix does not match the suffix + * of any of this enum's constants + */ + public static DataUnit fromSuffix(String suffix) { + for (DataUnit candidate : values()) { + if (candidate.suffix.equals(suffix)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown data unit suffix '" + suffix + "'"); + } + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerConfiguration.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerConfiguration.java new file mode 100644 index 0000000..3eb0aa1 --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerConfiguration.java @@ -0,0 +1,119 @@ +/* Copyright (c) 2022 Peigen.info. All rights reserved. */ + +package org.dromara.mica.mqtt.server.solon.config; + +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.deserialize.MqttJsonDeserializer; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.dromara.mica.mqtt.core.server.event.IMqttMessageListener; +import org.dromara.mica.mqtt.core.server.func.MqttFunctionManager; +import org.dromara.mica.mqtt.core.server.func.MqttFunctionMessageListener; +import org.dromara.mica.mqtt.server.solon.event.SolonEventMqttConnectStatusListener; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.tio.core.Node; + +/** + * (MqttServerConfiguration) + * + * @author LiHai + * @version 1.0.0 + * @since 2023/7/20 + */ +@Configuration +public class MqttServerConfiguration { + + @Bean + @Condition(onMissingBean = MqttDeserializer.class) + public MqttDeserializer mqttDeserializer() { + return new MqttJsonDeserializer(); + } + + @Bean + @Condition(onMissingBean = IMqttConnectStatusListener.class) + public IMqttConnectStatusListener connectStatusListener() { + return new SolonEventMqttConnectStatusListener(); + } + + @Bean + @Condition(onMissingBean = MqttFunctionManager.class) + public MqttFunctionManager mqttFunctionManager() { + return new MqttFunctionManager(); + } + + @Bean + @Condition(onMissingBean = IMqttMessageListener.class) + public IMqttMessageListener mqttFunctionMessageListener(MqttFunctionManager mqttFunctionManager) { + return new MqttFunctionMessageListener(mqttFunctionManager); + } + + @Bean + public MqttServerCreator mqttServerCreator(MqttServerProperties properties) { + MqttServerCreator serverCreator = MqttServer.create() + .name(properties.getName()) + .heartbeatTimeout(properties.getHeartbeatTimeout()) + .keepaliveBackoff(properties.getKeepaliveBackoff()) + .readBufferSize((int) DataSize.parse(properties.getReadBufferSize()).getBytes()) + .maxBytesInMessage((int) DataSize.parse(properties.getMaxBytesInMessage()).getBytes()) + .maxClientIdLength(properties.getMaxClientIdLength()) + .nodeName(properties.getNodeName()) + .statEnable(properties.isStatEnable()) + .proxyProtocolEnable(properties.isProxyProtocolOn()); + if (properties.isDebug()) { + serverCreator.debug(); + } + // mqtt 协议 + MqttServerProperties.Listener mqttListener = properties.getMqttListener(); + if (mqttListener.isEnable()) { + serverCreator.enableMqtt(builder -> builder.serverNode(mqttListener.getServerNode()).build()); + } + // mqtt ssl 协议 + MqttServerProperties.SslListener mqttSslListener = properties.getMqttSslListener(); + if (mqttSslListener.isEnable()) { + MqttServerProperties.Ssl ssl = mqttSslListener.getSsl(); + serverCreator.enableMqttSsl(sslBuilder -> sslBuilder + .serverNode(mqttSslListener.getServerNode()) + .useSsl(ssl.getKeystorePath(), ssl.getKeystorePass(), ssl.getTruststorePath(), ssl.getTruststorePass(), ssl.getClientAuth()) + .build()); + } + // mqtt websocket 协议 + MqttServerProperties.Listener wsListener = properties.getWsListener(); + if (wsListener.isEnable()) { + serverCreator.enableMqttWs(builder -> builder.serverNode(wsListener.getServerNode()).build()); + } + MqttServerProperties.SslListener wssListener = properties.getWssListener(); + if (mqttSslListener.isEnable()) { + MqttServerProperties.Ssl ssl = wssListener.getSsl(); + serverCreator.enableMqttWss(sslBuilder -> sslBuilder + .serverNode(wssListener.getServerNode()) + .useSsl(ssl.getKeystorePath(), ssl.getKeystorePass(), ssl.getTruststorePath(), ssl.getTruststorePass(), ssl.getClientAuth()) + .build()); + } + // mqtt http api + MqttServerProperties.HttpListener httpListener = properties.getHttpListener(); + if (httpListener.isEnable()) { + Node serverNode = httpListener.getServerNode(); + MqttServerProperties.HttpBasicAuth basicAuth = httpListener.getBasicAuth(); + MqttServerProperties.McpServer mcpServer = httpListener.getMcpServer(); + MqttServerProperties.HttpSsl ssl = httpListener.getSsl(); + serverCreator.enableMqttHttpApi(builder -> { + builder.serverNode(serverNode); + if (basicAuth.isEnable()) { + builder.basicAuth(basicAuth.getUsername(), basicAuth.getPassword()); + } + if (mcpServer.isEnable()) { + builder.mcpServer(mcpServer.getSseEndpoint(), mcpServer.getMessageEndpoint()); + } + if (ssl.isEnable()) { + builder.useSsl(ssl.getKeystorePath(), ssl.getKeystorePass(), ssl.getTruststorePath(), ssl.getTruststorePass(), ssl.getClientAuth()); + } + return builder.build(); + }); + } + return serverCreator; + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerMetrics.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerMetrics.java new file mode 100644 index 0000000..f248b7c --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerMetrics.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.server.solon.config; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.tio.core.Tio; +import org.tio.server.ServerGroupStat; +import org.tio.server.TioServerConfig; + +import java.util.Collections; + +/** + * mica mqtt Metrics + * + * @author L.cm + */ +@Slf4j +@RequiredArgsConstructor +public class MqttServerMetrics { + /** + * Prefix used for all mica-mqtt metric names. + */ + public static final String MQTT_METRIC_NAME_PREFIX = "mqtt"; + /** + * 连接统计 + */ + private static final String MQTT_CONNECTIONS_ACCEPTED = MQTT_METRIC_NAME_PREFIX + ".connections.accepted"; + private static final String MQTT_CONNECTIONS_SIZE = MQTT_METRIC_NAME_PREFIX + ".connections.size"; + private static final String MQTT_CONNECTIONS_CLOSED = MQTT_METRIC_NAME_PREFIX + ".connections.closed"; + /** + * 消息统计 + */ + private static final String MQTT_MESSAGES_HANDLED_PACKETS = MQTT_METRIC_NAME_PREFIX + ".messages.handled.packets"; + private static final String MQTT_MESSAGES_HANDLED_BYTES = MQTT_METRIC_NAME_PREFIX + ".messages.handled.bytes"; + private static final String MQTT_MESSAGES_RECEIVED_PACKETS = MQTT_METRIC_NAME_PREFIX + ".messages.received.packets"; + private static final String MQTT_MESSAGES_RECEIVED_BYTES = MQTT_METRIC_NAME_PREFIX + ".messages.received.bytes"; + private static final String MQTT_MESSAGES_SEND_PACKETS = MQTT_METRIC_NAME_PREFIX + ".messages.send.packets"; + private static final String MQTT_MESSAGES_SEND_BYTES = MQTT_METRIC_NAME_PREFIX + ".messages.send.bytes"; + + private final Iterable tags; + + public MqttServerMetrics() { + this(Collections.emptyList()); + } + + public void bindTo(MeterRegistry meterRegistry, TioServerConfig serverConfig) { + // 连接统计 + Gauge.builder(MQTT_CONNECTIONS_ACCEPTED, serverConfig, (config) -> ((ServerGroupStat) config.getGroupStat()).accepted.sum()) + .description("Mqtt server connections accepted") + .tags(tags) + .register(meterRegistry); + Gauge.builder(MQTT_CONNECTIONS_SIZE, serverConfig, (config) -> Tio.getAll(config).size()) + .description("Mqtt server connections size") + .tags(tags) + .register(meterRegistry); + Gauge.builder(MQTT_CONNECTIONS_CLOSED, serverConfig, (config) -> config.getGroupStat().getClosed().sum()) + .description("Mqtt server connections closed") + .tags(tags) + .register(meterRegistry); + // 消息统计 + Gauge.builder(MQTT_MESSAGES_HANDLED_PACKETS, serverConfig, (config) -> config.getGroupStat().getHandledPackets().sum()) + .description("Mqtt server handled packets") + .tags(tags) + .register(meterRegistry); + Gauge.builder(MQTT_MESSAGES_HANDLED_BYTES, serverConfig, (config) -> config.getGroupStat().getHandledBytes().sum()) + .description("Mqtt server handled bytes") + .tags(tags) + .register(meterRegistry); + // 接收的消息 + Gauge.builder(MQTT_MESSAGES_RECEIVED_PACKETS, serverConfig, (config) -> config.getGroupStat().getReceivedPackets().sum()) + .description("Mqtt server received packets") + .tags(tags) + .register(meterRegistry); + Gauge.builder(MQTT_MESSAGES_RECEIVED_BYTES, serverConfig, (config) -> config.getGroupStat().getReceivedBytes().sum()) + .description("Mqtt server received bytes") + .tags(tags) + .register(meterRegistry); + // 发送的消息 + Gauge.builder(MQTT_MESSAGES_SEND_PACKETS, serverConfig, (config) -> config.getGroupStat().getSentPackets().sum()) + .description("Mqtt server send packets") + .tags(tags) + .register(meterRegistry); + Gauge.builder(MQTT_MESSAGES_SEND_BYTES, serverConfig, (config) -> config.getGroupStat().getSentPackets().sum()) + .description("Mqtt server send bytes") + .tags(tags) + .register(meterRegistry); + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerMetricsConfiguration.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerMetricsConfiguration.java new file mode 100644 index 0000000..cbf818f --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerMetricsConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.server.solon.config; + +import io.micrometer.core.instrument.MeterRegistry; +import org.dromara.mica.mqtt.server.solon.MqttServerTemplate; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.event.AppLoadEndEvent; + +/** + * mica mqtt Metrics + * + * @author L.cm + */ +@Configuration +@Condition(onClass = MeterRegistry.class) +public class MqttServerMetricsConfiguration { + + @Bean + @Condition(onBean = MeterRegistry.class) + public MqttServerMetrics mqttServerMetrics(MeterRegistry registry, AppContext context) { + MqttServerMetrics metrics = new MqttServerMetrics(); + // 应用加载完成事件 + context.onEvent(AppLoadEndEvent.class, event -> { + MqttServerTemplate mqttServerTemplate = context.getBean(MqttServerTemplate.class); + metrics.bindTo(registry, mqttServerTemplate.getMqttServer().getServerConfig()); + }); + return metrics; + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerProperties.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerProperties.java new file mode 100644 index 0000000..ac210fc --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerProperties.java @@ -0,0 +1,240 @@ +/* Copyright (c) 2022 Peigen.info. All rights reserved. */ + +package org.dromara.mica.mqtt.server.solon.config; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.dromara.mica.mqtt.codec.MqttConstant; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; +import org.tio.core.Node; +import org.tio.core.ssl.ClientAuth; + +/** + * (MqttServerProperties) + * + * @author Lihai + * @version 1.0.0 + * @since 2023/7/19 + */ +@Inject(value = "${" + MqttServerProperties.PREFIX + "}", required = false) +@Configuration +@Data +public class MqttServerProperties { + /** + * 配置前缀 + */ + public static final String PREFIX = "mqtt.server"; + /** + * 是否启用,默认:启用 + */ + private boolean enabled = true; + /** + * 名称 + */ + private String name = "Mica-Mqtt-Server"; + /** + * mqtt 认证 + */ + private MqttAuth auth = new MqttAuth(); + /** + * 心跳超时时间(单位: 毫秒 默认: 1000 * 120),如果用户不希望框架层面做心跳相关工作,请把此值设为0或负数 + */ + private Long heartbeatTimeout; + /** + * MQTT 客户端 keepalive 系数,连接超时缺省为连接设置的 keepalive * keepaliveBackoff * 2,默认:0.75 + *

+ * 如果读者想对该值做一些调整,可以在此进行配置。比如设置为 0.75,则变为 keepalive * 1.5。但是该值不得小于 0.5,否则将小于 keepalive 设定的时间。 + */ + private float keepaliveBackoff = 0.75F; + /** + * 接收数据的 buffer size,默认:8KB + */ + private String readBufferSize = "8KB"; + /** + * 消息解析最大 bytes 长度,默认:10MB + */ + private String maxBytesInMessage = "10MB"; + /** + * debug + */ + private boolean debug = false; + /** + * mqtt 3.1 会校验此参数为 23,为了减少问题设置成了 64 + */ + private int maxClientIdLength = MqttConstant.DEFAULT_MAX_CLIENT_ID_LENGTH; + /** + * 节点名称,用于处理集群 + */ + private String nodeName; + /** + * 是否开启监控,不开启可节省内存,默认:true + */ + private boolean statEnable = true; + /** + * 开启代理协议,支持 nginx proxy_protocol on; + */ + private boolean proxyProtocolOn = false; + /** + * mqtt tcp 监听器 + */ + private Listener mqttListener = new Listener(); + /** + * mqtt tcp ssl 监听器 + */ + private SslListener mqttSslListener = new SslListener(); + /** + * websocket mqtt 监听器 + */ + private Listener wsListener = new Listener(); + /** + * websocket ssl mqtt 监听器 + */ + private SslListener wssListener = new SslListener(); + /** + * http api 监听器 + */ + private HttpListener httpListener = new HttpListener(); + + @Getter + @Setter + public static class MqttAuth { + /** + * 是否启用,默认:关闭 + */ + private boolean enable = false; + /** + * http Basic 认证账号 + */ + private String username; + /** + * http Basic 认证密码 + */ + private String password; + } + + @Getter + @Setter + public static class Listener { + /** + * 是否启用,默认:关闭 + */ + private boolean enable = false; + /** + * 服务端 ip + */ + private String ip; + /** + * 端口 + */ + private Integer port; + /** + * 获取服务节点 + * + * @return ServerNode + */ + public Node getServerNode() { + if (this.ip == null && this.port == null) { + return null; + } else { + return new Node(this.ip, this.port); + } + } + } + + @Getter + @Setter + public static class SslListener extends Listener { + /** + * ssl 配置 + */ + private Ssl ssl = new Ssl(); + } + + @Getter + @Setter + public static class Ssl { + /** + * keystore 证书路径 + */ + private String keystorePath; + /** + * keystore 密码 + */ + private String keystorePass; + /** + * truststore 证书路径 + */ + private String truststorePath; + /** + * truststore 密码 + */ + private String truststorePass; + /** + * 认证类型 + */ + private ClientAuth clientAuth = ClientAuth.NONE; + } + + @Getter + @Setter + public static class HttpListener extends Listener { + /** + * basic 认证 + */ + private HttpBasicAuth basicAuth = new HttpBasicAuth(); + /** + * mcp 配置 + */ + private McpServer mcpServer = new McpServer(); + /** + * ssl 配置 + */ + private HttpSsl ssl = new HttpSsl(); + } + + @Getter + @Setter + public static class HttpSsl extends Ssl { + /** + * 是否启用,默认:关闭 + */ + private boolean enable = false; + } + + @Getter + @Setter + public static class HttpBasicAuth { + /** + * 是否启用,默认:关闭 + */ + private boolean enable = false; + /** + * http Basic 认证账号 + */ + private String username; + /** + * http Basic 认证密码 + */ + private String password; + } + + @Getter + @Setter + public static class McpServer { + /** + * 是否启用,默认:关闭 + */ + private boolean enable = false; + /** + * sse 端点 + */ + private String sseEndpoint; + /** + * message 端点 + */ + private String messageEndpoint; + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/MqttClientOfflineEvent.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/MqttClientOfflineEvent.java new file mode 100644 index 0000000..44588ed --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/MqttClientOfflineEvent.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.server.solon.event; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 客户端断开原因 + * + * @author L.cm + */ +@Data +public class MqttClientOfflineEvent implements Serializable { + + /** + * 客户端 Id + */ + private String clientId; + /** + * 用户名 + */ + private String username; + /** + * 断开原因 + */ + private String reason; + /** + * 时间戳 + */ + private long ts; + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/MqttClientOnlineEvent.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/MqttClientOnlineEvent.java new file mode 100644 index 0000000..761e2e6 --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/MqttClientOnlineEvent.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.server.solon.event; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 客户端断开事件 + * + * @author L.cm + */ +@Data +public class MqttClientOnlineEvent implements Serializable { + + /** + * 客户端 id + */ + private String clientId; + /** + * 用户名 + */ + private String username; + /** + * ip + */ + private String ipAddress; + /** + * 端口 + */ + private int port; + /** + * keepalive + */ + private long keepalive; + /** + * 时间戳 + */ + private long ts; + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/SolonEventMqttConnectStatusListener.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/SolonEventMqttConnectStatusListener.java new file mode 100644 index 0000000..e9e9261 --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/SolonEventMqttConnectStatusListener.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.server.solon.event; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.noear.solon.core.event.EventBus; +import org.tio.core.ChannelContext; +import org.tio.core.Node; + +import java.util.concurrent.TimeUnit; + +/** + * spring event mqtt 连接状态 + * + * @author L.cm + */ +@Slf4j +public class SolonEventMqttConnectStatusListener implements IMqttConnectStatusListener { + + @Override + public void online(ChannelContext context, String clientId, String username) { + log.info("Mqtt clientId:{} username:{} online.", clientId, username); + MqttClientOnlineEvent onlineEvent = new MqttClientOnlineEvent(); + onlineEvent.setClientId(clientId); + onlineEvent.setUsername(username); + // clientNode + Node clientNode = context.getClientNode(); + onlineEvent.setIpAddress(clientNode.getIp()); + onlineEvent.setPort(clientNode.getPort()); + // keepalive + long keepalive = context.heartbeatTimeout == null ? 60L : TimeUnit.MILLISECONDS.toSeconds(context.heartbeatTimeout); + onlineEvent.setKeepalive(keepalive); + onlineEvent.setTs(context.stat.timeCreated); + EventBus.publish(onlineEvent); + } + + @Override + public void offline(ChannelContext context, String clientId, String username, String reason) { + log.info("Mqtt clientId:{} username:{} offline reason:{}.", clientId, username, reason); + MqttClientOfflineEvent offlineEvent = new MqttClientOfflineEvent(); + offlineEvent.setClientId(clientId); + offlineEvent.setUsername(username); + offlineEvent.setReason(reason); + offlineEvent.setTs(context.stat.timeClosed); + EventBus.publish(offlineEvent); + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/integration/MqttServerFunctionListener.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/integration/MqttServerFunctionListener.java new file mode 100644 index 0000000..c1fbe80 --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/integration/MqttServerFunctionListener.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.server.solon.integration; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.function.ParamValueFunction; +import org.dromara.mica.mqtt.core.server.func.IMqttFunctionMessageListener; +import org.dromara.mica.mqtt.core.util.MethodParamUtil; +import org.tio.core.ChannelContext; +import org.tio.utils.mica.ExceptionUtils; + +import java.lang.reflect.Method; + +/** + * mqtt 服务端函数消息监听器 + * + * @author L.cm + */ +class MqttServerFunctionListener implements IMqttFunctionMessageListener { + private final Object bean; + private final Method method; + private final ParamValueFunction[] paramValueFunctions; + + MqttServerFunctionListener(Object bean, Method method, String[] topicTemplates, String[] topicFilters, MqttDeserializer deserializer) { + this.bean = bean; + this.method = method; + this.paramValueFunctions = MethodParamUtil.getParamValueFunctions(method, topicTemplates, topicFilters, deserializer); + } + + @Override + public void onMessage(ChannelContext context, String clientId, String topic, MqttQoS qoS, MqttPublishMessage message) { + // 处理参数 + Object[] methodParameters = getMethodParameters(context, topic, message, message.payload()); + // 方法调用 + try { + method.invoke(bean, methodParameters); + } catch (Throwable e) { + throw ExceptionUtils.unchecked(e); + } + } + + + /** + * 获取反射参数 + * + * @param context context + * @param topic topic + * @param message message + * @param payload payload + * @return Object array + */ + protected Object[] getMethodParameters(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + int length = paramValueFunctions.length; + Object[] parameters = new Object[length]; + for (int i = 0; i < length; i++) { + ParamValueFunction function = paramValueFunctions[i]; + parameters[i] = function.getValue(context, topic, message, payload); + } + return parameters; + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/integration/MqttServerPluginImpl.java b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/integration/MqttServerPluginImpl.java new file mode 100644 index 0000000..4767cfe --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/integration/MqttServerPluginImpl.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.server.solon.integration; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.dromara.mica.mqtt.core.server.MqttServerCustomizer; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerAuthHandler; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerPublishPermission; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerSubscribeValidator; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerUniqueIdService; +import org.dromara.mica.mqtt.core.server.dispatcher.IMqttMessageDispatcher; +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.dromara.mica.mqtt.core.server.event.IMqttMessageListener; +import org.dromara.mica.mqtt.core.server.event.IMqttSessionListener; +import org.dromara.mica.mqtt.core.server.func.IMqttFunctionMessageListener; +import org.dromara.mica.mqtt.core.server.func.MqttFunctionManager; +import org.dromara.mica.mqtt.core.server.interceptor.IMqttMessageInterceptor; +import org.dromara.mica.mqtt.core.server.session.IMqttSessionManager; +import org.dromara.mica.mqtt.core.server.store.IMqttMessageStore; +import org.dromara.mica.mqtt.core.server.support.DefaultMqttServerAuthHandler; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.dromara.mica.mqtt.core.annotation.MqttServerFunction; +import org.dromara.mica.mqtt.server.solon.MqttServerTemplate; +import org.dromara.mica.mqtt.server.solon.config.MqttServerConfiguration; +import org.dromara.mica.mqtt.server.solon.config.MqttServerMetricsConfiguration; +import org.dromara.mica.mqtt.server.solon.config.MqttServerProperties; +import org.noear.solon.Solon; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.BeanWrap; +import org.noear.solon.core.Plugin; +import org.noear.solon.core.util.ClassUtil; + +import java.lang.reflect.Method; +import java.util.*; + +/** + * (MqttServerPluginImpl) + * + * @author LiHai + * @version 1.0.0 + * @since 2023/7/20 + */ +@Slf4j +public class MqttServerPluginImpl implements Plugin { + private final List> functionClassTags = new ArrayList<>(); + private final List> functionMethodTags = new ArrayList<>(); + private volatile boolean running = false; + private AppContext context; + + @Override + public void start(AppContext context) throws Throwable { + this.context = context; //todo: 去掉 Solon.context() 写法,可同时兼容 2.5 之前与之后的版本 + // 查找类上的 MqttServerFunction 注解 + context.beanBuilderAdd(MqttServerFunction.class, (clz, beanWrap, anno) -> { + functionClassTags.add(new ExtractorClassTag<>(clz, beanWrap, anno)); + }); + // 查找方法上的 MqttServerFunction 注解 + context.beanExtractorAdd(MqttServerFunction.class, (bw, method, anno) -> { + functionMethodTags.add(new ExtractorMethodTag<>(bw, method, anno)); + }); + context.lifecycle(-9, () -> { + context.beanMake(MqttServerProperties.class); + context.beanMake(MqttServerConfiguration.class); + MqttServerProperties properties = context.getBean(MqttServerProperties.class); + MqttServerCreator serverCreator = context.getBean(MqttServerCreator.class); + + IMqttServerAuthHandler authHandlerImpl = context.getBean(IMqttServerAuthHandler.class); + IMqttServerUniqueIdService uniqueIdService = context.getBean(IMqttServerUniqueIdService.class); + IMqttServerSubscribeValidator subscribeValidator = context.getBean(IMqttServerSubscribeValidator.class); + IMqttServerPublishPermission publishPermission = context.getBean(IMqttServerPublishPermission.class); + IMqttMessageDispatcher messageDispatcher = context.getBean(IMqttMessageDispatcher.class); + IMqttMessageStore messageStore = context.getBean(IMqttMessageStore.class); + IMqttSessionManager sessionManager = context.getBean(IMqttSessionManager.class); + IMqttSessionListener sessionListener = context.getBean(IMqttSessionListener.class); + IMqttMessageListener messageListener = context.getBean(IMqttMessageListener.class); + IMqttConnectStatusListener connectStatusListener = context.getBean(IMqttConnectStatusListener.class); + IMqttMessageInterceptor messageInterceptor = context.getBean(IMqttMessageInterceptor.class); + MqttServerCustomizer customizers = context.getBean(MqttServerCustomizer.class); + + // 自定义消息监听 + serverCreator.messageListener(messageListener); + // 认证处理器 + MqttServerProperties.MqttAuth mqttAuth = properties.getAuth(); + if (Objects.isNull(authHandlerImpl)) { + IMqttServerAuthHandler authHandler = mqttAuth.isEnable() ? new DefaultMqttServerAuthHandler(mqttAuth.getUsername(), mqttAuth.getPassword()) : null; + serverCreator.authHandler(authHandler); + } else { + serverCreator.authHandler(authHandlerImpl); + } + // mqtt 内唯一id + if (Objects.nonNull(uniqueIdService)) { + serverCreator.uniqueIdService(uniqueIdService); + } + // 订阅校验 + if (Objects.nonNull(subscribeValidator)) { + serverCreator.subscribeValidator(subscribeValidator); + } + // 订阅权限校验 + if (Objects.nonNull(publishPermission)) { + serverCreator.publishPermission(publishPermission); + } + // 消息转发 + if (Objects.nonNull(messageDispatcher)) { + serverCreator.messageDispatcher(messageDispatcher); + } + // 消息存储 + if (Objects.nonNull(messageStore)) { + serverCreator.messageStore(messageStore); + } + // session 管理 + if (Objects.nonNull(sessionManager)) { + serverCreator.sessionManager(sessionManager); + } + // session 监听 + if (Objects.nonNull(sessionListener)) { + serverCreator.sessionListener(sessionListener); + } + // 状态监听 + if (Objects.nonNull(connectStatusListener)) { + serverCreator.connectStatusListener(connectStatusListener); + } + // 消息监听器 + if (Objects.nonNull(messageInterceptor)) { + serverCreator.addInterceptor(messageInterceptor); + } + // 自定义处理 + if (Objects.nonNull(customizers)) { + customizers.customize(serverCreator); + } + MqttServer mqttServer = serverCreator.build(); + MqttServerTemplate mqttServerTemplate = new MqttServerTemplate(mqttServer); + context.wrapAndPut(MqttServerTemplate.class, mqttServerTemplate); + // Metrics + context.beanMake(MqttServerMetricsConfiguration.class); + // 添加启动时的函数处理 + functionDetector(); + // 启动 + if (properties.isEnabled() && !running) { + running = mqttServerTemplate.getMqttServer().start(); + log.info("mqtt server start..."); + } + }); + } + + private void functionDetector() { + // functionManager + MqttFunctionManager functionManager = context.getBean(MqttFunctionManager.class); + // 类级别的注解订阅 + functionClassTags.forEach(each -> { + MqttServerFunction anno = each.getAnno(); + String[] topicFilters = getTopicFilters(anno.value()); + IMqttFunctionMessageListener messageListener = each.getBeanWrap().get(); + functionManager.register(topicFilters, messageListener); + }); + // 方法级别的注解订阅 + functionMethodTags.forEach(each -> { + MqttServerFunction anno = each.getAnno(); + // topic 信息 + String[] topicTemplates = anno.value(); + String[] topicFilters = getTopicFilters(topicTemplates); + // 自定义的反序列化,支持 solon bean 或者 无参构造器初始化 + Class deserialized = anno.deserialize(); + MqttDeserializer deserializer = getMqttDeserializer(deserialized); + // 构造监听器 + Object bean = each.getBw().get(); + Method method = each.getMethod(); + // 注册监听器 + MqttServerFunctionListener functionListener = new MqttServerFunctionListener(bean, method, topicTemplates, topicFilters, deserializer); + functionManager.register(topicFilters, functionListener); + }); + } + + /** + * 获取解码器 + * + * @param deserializerType deserializerType + * @return 解码器 + */ + private MqttDeserializer getMqttDeserializer(Class deserializerType) { + BeanWrap beanWrap = context.getWrap(deserializerType); + if (beanWrap == null) { + return ClassUtil.newInstance(deserializerType); + } + return beanWrap.get(); + } + + private String[] getTopicFilters(String[] topicTemplates) { + // 1. 替换 solon cfg 变量 + // 2. 替换订阅中的其他变量 + return Arrays.stream(topicTemplates) + .map((x) -> Optional.ofNullable(Solon.cfg().getByTmpl(x)).orElse(x)) + .map(TopicUtil::getTopicFilter) + .toArray(String[]::new); + } + + @Override + public void stop() { + if (running) { + MqttServerTemplate mqttServerTemplate = context.getBean(MqttServerTemplate.class); + mqttServerTemplate.getMqttServer().stop(); + log.info("mqtt server stop..."); + } + } + + @Data + @RequiredArgsConstructor + private static class ExtractorClassTag { + private final Class clz; + private final BeanWrap beanWrap; + private final T anno; + } + + @Data + @RequiredArgsConstructor + private static class ExtractorMethodTag { + private final BeanWrap bw; + private final Method method; + private final T anno; + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/moditect/module-info.java b/starter/mica-mqtt-server-solon-plugin/src/main/moditect/module-info.java new file mode 100644 index 0000000..4e19dac --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/moditect/module-info.java @@ -0,0 +1,9 @@ +open module org.dromara.mica.mqtt.server.solon.plugin { + requires solon; + requires lombok; + requires transitive org.dromara.mica.mqtt.server; + exports org.dromara.mica.mqtt.server.noear; + exports org.dromara.mica.mqtt.server.solon.event; + exports org.dromara.mica.mqtt.server.solon.config; + provides org.noear.solon.core.Plugin with org.dromara.mica.mqtt.server.solon.integration.MqttServerPluginImpl; +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/resources/META-INF/solon/solon-configuration-metadata.json b/starter/mica-mqtt-server-solon-plugin/src/main/resources/META-INF/solon/solon-configuration-metadata.json new file mode 100644 index 0000000..d97be41 --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/resources/META-INF/solon/solon-configuration-metadata.json @@ -0,0 +1,269 @@ +{ + "properties": [ + { + "name": "mqtt.server.auth.enable", + "type": "java.lang.Boolean", + "description": "是否启用,默认:关闭", + "defaultValue": "false" + }, + { + "name": "mqtt.server.auth.password", + "type": "java.lang.String", + "description": "http Basic 认证密码" + }, + { + "name": "mqtt.server.auth.username", + "type": "java.lang.String", + "description": "http Basic 认证账号" + }, + { + "name": "mqtt.server.debug", + "type": "java.lang.Boolean", + "description": "debug", + "defaultValue": "false" + }, + { + "name": "mqtt.server.enabled", + "type": "java.lang.Boolean", + "description": "是否启用,默认:启用", + "defaultValue": "true" + }, + { + "name": "mqtt.server.heartbeat-timeout", + "type": "java.lang.Long", + "description": "心跳超时时间(单位: 毫秒 默认: 1000 * 120),如果用户不希望框架层面做心跳相关工作,请把此值设为0或负数" + }, + { + "name": "mqtt.server.http-listener.basic-auth.enable", + "type": "java.lang.Boolean", + "description": "是否启用,默认:关闭", + "defaultValue": "false" + }, + { + "name": "mqtt.server.http-listener.basic-auth.password", + "type": "java.lang.String", + "description": "http Basic 认证密码" + }, + { + "name": "mqtt.server.http-listener.basic-auth.username", + "type": "java.lang.String", + "description": "http Basic 认证账号" + }, + { + "name": "mqtt.server.http-listener.enable", + "type": "java.lang.Boolean", + "description": "是否启用,默认:关闭", + "defaultValue": "false" + }, + { + "name": "mqtt.server.http-listener.ip", + "type": "java.lang.String", + "description": "服务端 ip" + }, + { + "name": "mqtt.server.http-listener.mcp-server.enable", + "type": "java.lang.Boolean", + "description": "是否启用,默认:关闭", + "defaultValue": "false" + }, + { + "name": "mqtt.server.http-listener.mcp-server.message-endpoint", + "type": "java.lang.String", + "description": "message 端点" + }, + { + "name": "mqtt.server.http-listener.mcp-server.sse-endpoint", + "type": "java.lang.String", + "description": "sse 端点" + }, + { + "name": "mqtt.server.http-listener.port", + "type": "java.lang.Integer", + "description": "端口" + }, + { + "name": "mqtt.server.http-listener.ssl.client-auth", + "type": "org.tio.core.ssl.ClientAuth", + "description": "认证类型" + }, + { + "name": "mqtt.server.http-listener.ssl.enable", + "type": "java.lang.Boolean", + "description": "是否启用,默认:关闭", + "defaultValue": "false" + }, + { + "name": "mqtt.server.http-listener.ssl.keystore-pass", + "type": "java.lang.String", + "description": "keystore 密码" + }, + { + "name": "mqtt.server.http-listener.ssl.keystore-path", + "type": "java.lang.String", + "description": "keystore 证书路径" + }, + { + "name": "mqtt.server.http-listener.ssl.truststore-pass", + "type": "java.lang.String", + "description": "truststore 密码" + }, + { + "name": "mqtt.server.http-listener.ssl.truststore-path", + "type": "java.lang.String", + "description": "truststore 证书路径" + }, + { + "name": "mqtt.server.keepalive-backoff", + "type": "java.lang.Float", + "description": "MQTT 客户端 keepalive 系数,连接超时缺省为连接设置的 keepalive * keepaliveBackoff * 2,默认:0.75

如果读者想对该值做一些调整,可以在此进行配置。比如设置为 0.75,则变为 keepalive * 1.5。但是该值不得小于 0.5,否则将小于 keepalive 设定的时间。" + }, + { + "name": "mqtt.server.max-bytes-in-message", + "type": "java.lang.String", + "description": "消息解析最大 bytes 长度,默认:10M" + }, + { + "name": "mqtt.server.max-client-id-length", + "type": "java.lang.Integer", + "description": "mqtt 3.1 会校验此参数为 23,为了减少问题设置成了 64" + }, + { + "name": "mqtt.server.mqtt-listener.enable", + "type": "java.lang.Boolean", + "description": "是否启用,默认:关闭", + "defaultValue": "false" + }, + { + "name": "mqtt.server.mqtt-listener.ip", + "type": "java.lang.String", + "description": "服务端 ip" + }, + { + "name": "mqtt.server.mqtt-listener.port", + "type": "java.lang.Integer", + "description": "端口" + }, + { + "name": "mqtt.server.mqtt-ssl-listener.enable", + "type": "java.lang.Boolean", + "description": "是否启用,默认:关闭", + "defaultValue": "false" + }, + { + "name": "mqtt.server.mqtt-ssl-listener.ip", + "type": "java.lang.String", + "description": "服务端 ip" + }, + { + "name": "mqtt.server.mqtt-ssl-listener.port", + "type": "java.lang.Integer", + "description": "端口" + }, + { + "name": "mqtt.server.mqtt-ssl-listener.ssl.client-auth", + "type": "org.tio.core.ssl.ClientAuth", + "description": "认证类型" + }, + { + "name": "mqtt.server.mqtt-ssl-listener.ssl.keystore-pass", + "type": "java.lang.String", + "description": "keystore 密码" + }, + { + "name": "mqtt.server.mqtt-ssl-listener.ssl.keystore-path", + "type": "java.lang.String", + "description": "keystore 证书路径" + }, + { + "name": "mqtt.server.mqtt-ssl-listener.ssl.truststore-pass", + "type": "java.lang.String", + "description": "truststore 密码" + }, + { + "name": "mqtt.server.mqtt-ssl-listener.ssl.truststore-path", + "type": "java.lang.String", + "description": "truststore 证书路径" + }, + { + "name": "mqtt.server.name", + "type": "java.lang.String", + "description": "名称" + }, + { + "name": "mqtt.server.node-name", + "type": "java.lang.String", + "description": "节点名称,用于处理集群" + }, + { + "name": "mqtt.server.proxy-protocol-on", + "type": "java.lang.Boolean", + "description": "开启代理协议,支持 nginx proxy_protocol on;", + "defaultValue": "false" + }, + { + "name": "mqtt.server.read-buffer-size", + "type": "java.lang.String", + "description": "接收数据的 buffer size,默认:8k" + }, + { + "name": "mqtt.server.stat-enable", + "type": "java.lang.Boolean", + "description": "是否开启监控,不开启可节省内存,默认:true" + }, + { + "name": "mqtt.server.ws-listener.enable", + "type": "java.lang.Boolean", + "description": "是否启用,默认:关闭" + }, + { + "name": "mqtt.server.ws-listener.ip", + "type": "java.lang.String", + "description": "服务端 ip" + }, + { + "name": "mqtt.server.ws-listener.port", + "type": "java.lang.Integer", + "description": "端口" + }, + { + "name": "mqtt.server.wss-listener.enable", + "type": "java.lang.Boolean", + "description": "是否启用,默认:关闭" + }, + { + "name": "mqtt.server.wss-listener.ip", + "type": "java.lang.String", + "description": "服务端 ip" + }, + { + "name": "mqtt.server.wss-listener.port", + "type": "java.lang.Integer", + "description": "端口" + }, + { + "name": "mqtt.server.wss-listener.ssl.client-auth", + "type": "org.tio.core.ssl.ClientAuth", + "description": "认证类型" + }, + { + "name": "mqtt.server.wss-listener.ssl.keystore-pass", + "type": "java.lang.String", + "description": "keystore 密码" + }, + { + "name": "mqtt.server.wss-listener.ssl.keystore-path", + "type": "java.lang.String", + "description": "keystore 证书路径" + }, + { + "name": "mqtt.server.wss-listener.ssl.truststore-pass", + "type": "java.lang.String", + "description": "truststore 密码" + }, + { + "name": "mqtt.server.wss-listener.ssl.truststore-path", + "type": "java.lang.String", + "description": "truststore 证书路径" + } + ] +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/main/resources/META-INF/solon/solon.mica.server.properties b/starter/mica-mqtt-server-solon-plugin/src/main/resources/META-INF/solon/solon.mica.server.properties new file mode 100644 index 0000000..25dc2a8 --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/main/resources/META-INF/solon/solon.mica.server.properties @@ -0,0 +1,2 @@ +solon.plugin=org.dromara.mica.mqtt.server.solon.integration.MqttServerPluginImpl +solon.plugin.priority=1 diff --git a/starter/mica-mqtt-server-solon-plugin/src/test/java/org/dromara/mica/mqtt/server/solon/test/task/ServerTest.java b/starter/mica-mqtt-server-solon-plugin/src/test/java/org/dromara/mica/mqtt/server/solon/test/task/ServerTest.java new file mode 100644 index 0000000..ec0db69 --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/test/java/org/dromara/mica/mqtt/server/solon/test/task/ServerTest.java @@ -0,0 +1,20 @@ +package org.dromara.mica.mqtt.server.solon.test.task; + +import org.noear.solon.Solon; +import org.noear.solon.scheduling.annotation.EnableScheduling; + +/** + * (ServerTest) + * + * @author Peigen + * @version 1.0.0 + * @since 2023/7/15 + */ +@EnableScheduling +public class ServerTest { + + public static void main(String[] args) { + Solon.start(ServerTest.class,args); + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/test/java/org/dromara/mica/mqtt/server/solon/test/task/task/PublishAllTask.java b/starter/mica-mqtt-server-solon-plugin/src/test/java/org/dromara/mica/mqtt/server/solon/test/task/task/PublishAllTask.java new file mode 100644 index 0000000..b4ca086 --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/test/java/org/dromara/mica/mqtt/server/solon/test/task/task/PublishAllTask.java @@ -0,0 +1,24 @@ +package org.dromara.mica.mqtt.server.solon.test.task.task; + +import org.dromara.mica.mqtt.server.solon.MqttServerTemplate; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; +import org.noear.solon.scheduling.annotation.Scheduled; + +import java.nio.charset.StandardCharsets; + +/** + * @author wsq + */ +@Component +public class PublishAllTask { + @Inject + private MqttServerTemplate mqttServerTemplate; + + @Scheduled(fixedDelay = 1000) + public void run() { + boolean b = mqttServerTemplate.publishAll("/test/123", "mica最牛皮".getBytes(StandardCharsets.UTF_8)); + System.out.println(b); + } + +} diff --git a/starter/mica-mqtt-server-solon-plugin/src/test/resources/app.yml b/starter/mica-mqtt-server-solon-plugin/src/test/resources/app.yml new file mode 100644 index 0000000..dca837c --- /dev/null +++ b/starter/mica-mqtt-server-solon-plugin/src/test/resources/app.yml @@ -0,0 +1,20 @@ +server: + port: 30033 +# mqtt 服务端配置 +mqtt: + server: + enabled: true # 是否开启服务端,默认:true + name: Mica-Mqtt-Server # 名称,默认:Mica-Mqtt-Server + heartbeat-timeout: 120000 # 心跳超时,单位毫秒,默认: 1000 * 120 + read-buffer-size: 8KB # 接收数据的 buffer size,默认:8k + max-bytes-in-message: 10MB # 消息解析最大 bytes 长度,默认:10M + auth: + enable: false # 是否开启 mqtt 认证 + username: mica # mqtt 认证用户名 + password: mica # mqtt 认证密码 + debug: true # 如果开启 prometheus 指标收集建议关闭 + stat-enable: true # 开启指标收集,debug 和 prometheus 开启时需要打开,默认开启,关闭节省内存 + mqtt-listener: # mqtt 监听器,还有 mqtt-ssl-listener + enable: true # 是否开启,默认:false + # ip: "0.0.0.0" # 服务端 ip 默认为空,0.0.0.0,建议不要设置 + port: 1883 # 端口,默认:1883 diff --git a/starter/mica-mqtt-server-spring-boot-starter/README.md b/starter/mica-mqtt-server-spring-boot-starter/README.md new file mode 100644 index 0000000..f2ab889 --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/README.md @@ -0,0 +1,202 @@ +# mica-mqtt-server-spring-boot-starter 使用文档 + +## 版本兼容 +| 要求 | Spring boot 版本 | +|-----|----------------| +| 最高 | 4.x | +| 最低 | 2.1.0.RELEASE | + +## 一、添加依赖 + +```xml + + org.dromara.mica-mqtt + mica-mqtt-server-spring-boot-starter + ${最新版本} + +``` + +## 二、mqtt 服务 + +### 2.1 配置项 + +```yaml +# mqtt 服务端配置 +mqtt: + server: + enabled: true # 是否开启服务端,默认:true + name: Mica-Mqtt-Server # 名称,默认:Mica-Mqtt-Server + heartbeat-timeout: 120000 # 心跳超时,单位毫秒,默认: 1000 * 120 + read-buffer-size: 8KB # 接收数据的 buffer size,默认:8k + max-bytes-in-message: 10MB # 消息解析最大 bytes 长度,默认:10M + auth: + enable: false # 是否开启 mqtt 认证 + username: mica # mqtt 认证用户名 + password: mica # mqtt 认证密码 + debug: true # 如果开启 prometheus 指标收集建议关闭 + stat-enable: true # 开启指标收集,debug 和 prometheus 开启时需要打开,默认开启,关闭节省内存 + mqtt-listener: # mqtt 监听器 + enable: true # 是否开启,默认:false +# ip: "0.0.0.0" # 服务端 ip 默认为空,0.0.0.0,建议不要设置 + port: 1883 # 端口,默认:1883 + mqtt-ssl-listener: # mqtt ssl 监听器 + enable: false # 是否开启,默认:false + port: 8883 # 端口,默认:8883 + ssl: # ssl 配置,必须 + keystore-path: # 必须参数:ssl keystore 目录,支持 classpath:/ 路径。 + keystore-pass: # 必选参数:ssl keystore 密码 + truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 + truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 + client-auth: none # 是否需要客户端认证(双向认证),默认:NONE(不需要) + ws-listener: # websocket mqtt 监听器 + enable: true # 是否开启,默认:false + port: 8083 # websocket 端口,默认:8083 + wss-listener: # websocket ssl mqtt 监听器 + enable: false # 是否开启,默认:false + port: 8084 # 端口,默认:8084 + ssl: # ssl 配置,必须 + keystore-path: # 必须参数:ssl keystore 目录,支持 classpath:/ 路径。 + keystore-pass: # 必选参数:ssl keystore 密码 + truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 + truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 + client-auth: none # 是否需要客户端认证(双向认证),默认:NONE(不需要) + http-listener: + enable: true + port: 18083 + basic-auth: # 基础认证 + enable: true + username: mica + password: mica + mcp-server: # 大模型 mcp + enable: true +``` + +注意:**ssl** 存在三种情况 + +| 服务端开启ssl | 客户端 | +| ---------------------------------------- | --------------------------------------------- | +| ClientAuth 为 NONE(不需要客户端验证) | 仅仅需要开启 ssl 即可不用配置证书 | +| ClientAuth 为 OPTIONAL(与客户端协商) | 需开启 ssl 并且配置 truststore 证书 | +| ClientAuth 为 REQUIRE (必须的客户端验证) | 需开启 ssl 并且配置 truststore、 keystore证书 | + +### 2.2 可实现接口(注册成 Spring Bean 即可) + +| 接口 | 是否必须 | 说明 | +|-------------------------------|------------|-----------------------------------------------| +| IMqttServerUniqueIdService | 否 | 用于 clientId 不唯一时,自定义实现唯一标识,后续接口使用它替代 clientId | +| IMqttServerAuthHandler | 是 | 用于服务端认证 | +| IMqttServerSubscribeValidator | 否(建议实现) | 1.1.3 新增,用于对客户端订阅校验 | +| IMqttServerPublishPermission | 否(建议实现) | 1.2.2 新增,用于对客户端发布权限校验 | +| IMqttMessageListener | 否(1.3.x为否) | 消息监听 | +| IMqttConnectStatusListener | 是 | 连接状态监听 | +| IMqttSessionManager | 否 | session 管理 | +| IMqttSessionListener | 否 | session 监听 | +| IMqttMessageStore | 集群是,单机否 | 遗嘱和保留消息存储 | +| AbstractMqttMessageDispatcher | 集群是,单机否 | 消息转发,(遗嘱、保留消息转发) | +| IMqttMessageInterceptor | 否 | 消息拦截器,1.3.9 新增 | + +### 2.3 IMqttMessageListener (用于监听客户端上传的消息) 使用示例 + +```java +@Service +public class MqttServerMessageListener implements IMqttMessageListener { + private static final Logger logger = LoggerFactory.getLogger(MqttServerMessageListener.class); + + @Override + public void onMessage(ChannelContext context, String clientId, String topic, MqttQoS qoS, MqttPublishMessage message) { + log.info("clientId:{} message:{} payload:{}", clientId, message, new String(message.getPayload(), StandardCharsets.UTF_8)); + } +} +``` + +### 2.4 自定义配置(可选) + +```java +@Configuration(proxyBeanMethods = false) +public class MqttServerCustomizerConfiguration { + + @Bean + public MqttServerCustomizer mqttServerCustomizer() { + return new MqttServerCustomizer() { + @Override + public void customize(MqttServerCreator creator) { + // 此处可自定义配置 creator,会覆盖 yml 中的配置 + System.out.println("----------------MqttServerCustomizer-----------------"); + } + }; + } + +} +``` + +### 2.5 MqttServerTemplate 使用示例 + +```java + +import org.dromara.mica.mqtt.spring.server.MqttServerTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * @author wsq + */ +@Service +public class ServerService { + @Autowired + private MqttServerTemplate server; + + public boolean publish(String body) { + server.publishAll("/test/123", body.getBytes(StandardCharsets.UTF_8)); + return true; + } +} +``` + +### 2.6 客户端上下线监听 +使用 Spring event 解耦客户端上下线监听,注意: `1.3.4` 开始支持。会跟自定义的 `IMqttConnectStatusListener` 实现冲突,取一即可。 + +```java +@Service +public class MqttConnectStatusListener { + private static final Logger logger = LoggerFactory.getLogger(MqttConnectStatusListener.class); + + @EventListener + public void online(MqttClientOnlineEvent event) { + logger.info("MqttClientOnlineEvent:{}", event); + } + + @EventListener + public void offline(MqttClientOfflineEvent event) { + logger.info("MqttClientOfflineEvent:{}", event); + } + +} +``` + +### 2.7 基于 mq 消息广播集群处理 + +详见: [mica-mqtt-broker](../../mica-mqtt-broker) + +### 2.8 Prometheus + Grafana 监控对接 +```xml + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + +``` + +| 支持的指标 | 说明 | +|--------------------------------| ---------------- | +| mqtt_connections_accepted | 共接受过连接数 | +| mqtt_connections_closed | 关闭过的连接数 | +| mqtt_connections_size | 当前连接数 | +| mqtt_messages_handled_packets | 已处理消息数 | +| mqtt_messages_handled_bytes | 已处理消息字节数 | +| mqtt_messages_received_packets | 已接收消息数 | +| mqtt_messages_received_bytes | 已处理消息字节数 | +| mqtt_messages_send_packets | 已发送消息数 | +| mqtt_messages_send_bytes | 已发送消息字节数 | diff --git a/starter/mica-mqtt-server-spring-boot-starter/pom.xml b/starter/mica-mqtt-server-spring-boot-starter/pom.xml new file mode 100644 index 0000000..202faf9 --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + starter + ${revision} + + mica-mqtt-server-spring-boot-starter + ${project.artifactId} + https://mica-mqtt.dreamlu.net/guide/spring/server.html + + + + org.dromara.mica-mqtt + mica-mqtt-server + + + org.springframework.boot + spring-boot-autoconfigure + provided + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + provided + + + io.micrometer + micrometer-core + provided + + + + org.projectlombok + lombok + provided + + + + net.dreamlu + mica-auto + provided + + + + diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionDetector.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionDetector.java new file mode 100644 index 0000000..67a53fd --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionDetector.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.annotation.MqttServerFunction; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.server.func.IMqttFunctionMessageListener; +import org.dromara.mica.mqtt.core.server.func.MqttFunctionManager; +import org.dromara.mica.mqtt.core.util.TopicUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.NonNull; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Modifier; +import java.util.Arrays; + +/** + * Mqtt 服务端消息处理 + * + * @author L.cm + */ +@Slf4j +@RequiredArgsConstructor +public class MqttServerFunctionDetector implements BeanPostProcessor { + private final ApplicationContext applicationContext; + + @Override + public Object postProcessAfterInitialization(@NonNull Object bean, String beanName) throws BeansException { + Class userClass = ClassUtils.getUserClass(bean); + // 1. 查找类上的 MqttServerFunction 注解 + if (bean instanceof IMqttFunctionMessageListener) { + MqttServerFunction subscribe = AnnotationUtils.findAnnotation(userClass, MqttServerFunction.class); + if (subscribe != null) { + String[] topicFilters = getTopicFilters(applicationContext, subscribe.value()); + // 3. 注册监听器 + MqttFunctionManager functionManager = getFunctionManager(); + functionManager.register(topicFilters, (IMqttFunctionMessageListener) bean); + } + } else { + // 2. 查找方法上的 MqttServerFunction 注解 + ReflectionUtils.doWithMethods(userClass, method -> { + MqttServerFunction subscribe = AnnotationUtils.findAnnotation(method, MqttServerFunction.class); + if (subscribe != null) { + // 1. 校验必须为 public 和非 static 的方法 + int modifiers = method.getModifiers(); + if (Modifier.isStatic(modifiers)) { + throw new IllegalArgumentException("@MqttServerFunction on method " + method + " must not static."); + } + if (!Modifier.isPublic(modifiers)) { + throw new IllegalArgumentException("@MqttServerFunction on method " + method + " must public."); + } + // 2. 校验 method 入参数必须等于2 + int paramCount = method.getParameterCount(); + if (paramCount < 2 || paramCount > 6) { + throw new IllegalArgumentException("@MqttServerFunction on method " + method + " parameter count must 2 ~ 6."); + } + // 3. topic 信息 + String[] topicTemplates = subscribe.value(); + String[] topicFilters = getTopicFilters(applicationContext, topicTemplates); + // 4. 自定义的反序列化,支持 Spring bean 或者 无参构造器初始化 + Class deserialized = subscribe.deserialize(); + @SuppressWarnings("unchecked") + MqttDeserializer deserializer = getMqttDeserializer((Class) deserialized); + // 5. 监听器 + MqttServerFunctionListener functionListener = new MqttServerFunctionListener(bean, method, topicTemplates, topicFilters, deserializer); + // 6. 注册监听器 + MqttFunctionManager functionManager = getFunctionManager(); + functionManager.register(topicFilters, functionListener); + } + }, ReflectionUtils.USER_DECLARED_METHODS); + } + return bean; + } + + /** + * 获取 MqttFunctionManager + * + * @return MqttFunctionManager + */ + protected MqttFunctionManager getFunctionManager() { + return applicationContext.getBean(MqttFunctionManager.class); + } + + /** + * 获取解码器 + * + * @param deserializerType deserializerType + * @return 解码器 + */ + protected MqttDeserializer getMqttDeserializer(Class deserializerType) { + return applicationContext.getBeanProvider(deserializerType) + .getIfAvailable(() -> BeanUtils.instantiateClass(deserializerType)); + } + + /** + * 解析 Spring boot env 变量 + * + * @param applicationContext ApplicationContext + * @param values values + * @return topic array + */ + private static String[] getTopicFilters(ApplicationContext applicationContext, String[] values) { + // 1. 替换 Spring boot env 变量 + // 2. 替换订阅中的其他变量 + return Arrays.stream(values) + .map(applicationContext.getEnvironment()::resolvePlaceholders) + .map(TopicUtil::getTopicFilter) + .toArray(String[]::new); + } + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionLazyFilter.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionLazyFilter.java new file mode 100644 index 0000000..c1970f3 --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionLazyFilter.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server; + +import org.dromara.mica.mqtt.core.annotation.MqttServerFunction; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.LazyInitializationExcludeFilter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * mqtt 服务端函数延迟加载排除 + * + * @author L.cm + */ +public class MqttServerFunctionLazyFilter implements LazyInitializationExcludeFilter { + + @Override + public boolean isExcluded(String beanName, BeanDefinition beanDefinition, Class beanType) { + // 类上有注解的情况 + MqttServerFunction subscribe = AnnotationUtils.findAnnotation(beanType, MqttServerFunction.class); + if (subscribe != null) { + return true; + } + // 方法上的注解 + List methodList = new ArrayList<>(); + ReflectionUtils.doWithMethods(beanType, method -> { + MqttServerFunction clientSubscribe = AnnotationUtils.findAnnotation(method, MqttServerFunction.class); + if (clientSubscribe != null) { + methodList.add(method); + } + }, ReflectionUtils.USER_DECLARED_METHODS); + return !methodList.isEmpty(); + } + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionListener.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionListener.java new file mode 100644 index 0000000..b806b8a --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionListener.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.function.ParamValueFunction; +import org.dromara.mica.mqtt.core.server.func.IMqttFunctionMessageListener; +import org.dromara.mica.mqtt.core.util.MethodParamUtil; +import org.springframework.util.ReflectionUtils; +import org.tio.core.ChannelContext; + +import java.lang.reflect.Method; + +/** + * mqtt 服务端函数消息监听器 + * + * @author L.cm + */ +class MqttServerFunctionListener implements IMqttFunctionMessageListener { + private final Object bean; + private final Method method; + private final ParamValueFunction[] paramValueFunctions; + + MqttServerFunctionListener(Object bean, Method method, String[] topicTemplates, String[] topicFilters, MqttDeserializer deserializer) { + this.bean = bean; + this.method = method; + this.paramValueFunctions = MethodParamUtil.getParamValueFunctions(method, topicTemplates, topicFilters, deserializer); + } + + @Override + public void onMessage(ChannelContext context, String clientId, String topic, MqttQoS qoS, MqttPublishMessage message) { + // 处理参数 + Object[] methodParameters = getMethodParameters(context, topic, message, message.payload()); + // 反射调用 + ReflectionUtils.invokeMethod(method, bean, methodParameters); + } + + /** + * 获取反射参数 + * + * @param context context + * @param topic topic + * @param message message + * @param payload payload + * @return Object array + */ + protected Object[] getMethodParameters(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + int length = paramValueFunctions.length; + Object[] parameters = new Object[length]; + for (int i = 0; i < length; i++) { + ParamValueFunction function = paramValueFunctions[i]; + parameters[i] = function.getValue(context, topic, message, payload); + } + return parameters; + } + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerTemplate.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerTemplate.java new file mode 100644 index 0000000..4fecae6 --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerTemplate.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.core.server.model.ClientInfo; +import org.dromara.mica.mqtt.core.server.model.Subscribe; +import org.tio.core.ChannelContext; +import org.tio.core.stat.vo.StatVo; +import org.tio.utils.page.Page; +import org.tio.utils.timer.TimerTask; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * mqtt Server 模板 + * + * @author wsq(冷月宫主) + * @author ChangJin Wei (魏昌进) + */ +@Getter +@RequiredArgsConstructor +public class MqttServerTemplate { + private final MqttServer mqttServer; + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload) { + return mqttServer.publish(clientId, topic, payload); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload, MqttQoS qos) { + return mqttServer.publish(clientId, topic, payload, qos); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload, boolean retain) { + return mqttServer.publish(clientId, topic, payload, retain); + } + + /** + * 发布消息 + * + * @param clientId clientId + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publish(String clientId, String topic, Object payload, MqttQoS qos, boolean retain) { + return mqttServer.publish(clientId, topic, payload, qos, retain); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload) { + return mqttServer.publishAll(topic, payload); + } + + /** + * 发布消息 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload, MqttQoS qos) { + return mqttServer.publishAll(topic, payload, qos); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload, boolean retain) { + return mqttServer.publishAll(topic, payload, retain); + } + + /** + * 发布消息给所以的在线设备 + * + * @param topic topic + * @param payload 消息体 + * @param qos MqttQoS + * @param retain 是否在服务器上保留消息 + * @return 是否发送成功 + */ + public boolean publishAll(String topic, Object payload, MqttQoS qos, boolean retain) { + return mqttServer.publishAll(topic, payload, qos, retain); + } + + /** + * 获取客户端信息 + * + * @param clientId clientId + * @return ClientInfo + */ + public ClientInfo getClientInfo(String clientId) { + return mqttServer.getClientInfo(clientId); + } + + /** + * 获取客户端信息 + * + * @param context ChannelContext + * @return ClientInfo + */ + public ClientInfo getClientInfo(ChannelContext context) { + return mqttServer.getClientInfo(context); + } + + /** + * 获取所有的客户端 + * + * @return 客户端列表 + */ + public List getClients() { + return mqttServer.getClients(); + } + + /** + * 分页获取所有的客户端 + * + * @param pageIndex pageIndex,默认为 1 + * @param pageSize pageSize,默认为所有 + * @return 分页 + */ + public Page getClients(Integer pageIndex, Integer pageSize) { + return mqttServer.getClients(pageIndex, pageSize); + } + + /** + * 获取统计数据 + * @return StatVo + */ + public StatVo getStat() { + return mqttServer.getStat(); + } + + /** + * 获取客户端订阅情况 + * + * @param clientId clientId + * @return 订阅集合 + */ + public List getSubscriptions(String clientId) { + return mqttServer.getSubscriptions(clientId); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay) { + return mqttServer.schedule(command, delay); + } + + /** + * 添加定时任务,注意:如果抛出异常,会终止后续任务,请自行处理异常 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask schedule(Runnable command, long delay, Executor executor) { + return mqttServer.schedule(command, delay, executor); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay) { + return mqttServer.scheduleOnce(command, delay); + } + + /** + * 添加定时任务 + * + * @param command runnable + * @param delay delay + * @param executor 用于自定义线程池,处理耗时业务 + * @return TimerTask + */ + public TimerTask scheduleOnce(Runnable command, long delay, Executor executor) { + return mqttServer.scheduleOnce(command, delay, executor); + } + + /** + * 获取 ChannelContext + * + * @param clientId clientId + * @return ChannelContext + */ + public ChannelContext getChannelContext(String clientId) { + return mqttServer.getChannelContext(clientId); + } + + /** + * 服务端主动断开连接 + * + * @param clientId clientId + */ + public void close(String clientId) { + mqttServer.close(clientId); + } + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerConfiguration.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerConfiguration.java new file mode 100644 index 0000000..a6a4929 --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerConfiguration.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server.config; + +import org.dromara.mica.mqtt.core.deserialize.MqttDeserializer; +import org.dromara.mica.mqtt.core.deserialize.MqttJsonDeserializer; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.core.server.MqttServerCreator; +import org.dromara.mica.mqtt.core.server.MqttServerCustomizer; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerAuthHandler; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerPublishPermission; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerSubscribeValidator; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerUniqueIdService; +import org.dromara.mica.mqtt.core.server.dispatcher.IMqttMessageDispatcher; +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.dromara.mica.mqtt.core.server.event.IMqttMessageListener; +import org.dromara.mica.mqtt.core.server.event.IMqttSessionListener; +import org.dromara.mica.mqtt.core.server.func.MqttFunctionManager; +import org.dromara.mica.mqtt.core.server.func.MqttFunctionMessageListener; +import org.dromara.mica.mqtt.core.server.interceptor.IMqttMessageInterceptor; +import org.dromara.mica.mqtt.core.server.session.IMqttSessionManager; +import org.dromara.mica.mqtt.core.server.store.IMqttMessageStore; +import org.dromara.mica.mqtt.core.server.support.DefaultMqttServerAuthHandler; +import org.dromara.mica.mqtt.spring.server.MqttServerFunctionDetector; +import org.dromara.mica.mqtt.spring.server.MqttServerTemplate; +import org.dromara.mica.mqtt.spring.server.event.SpringEventMqttConnectStatusListener; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.tio.core.Node; + +/** + * mqtt server 配置 + * + * @author L.cm + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty( + prefix = MqttServerProperties.PREFIX, + name = "enabled", + havingValue = "true", + matchIfMissing = true +) +@EnableConfigurationProperties(MqttServerProperties.class) +public class MqttServerConfiguration { + + @Bean + @ConditionalOnMissingBean + public MqttDeserializer mqttDeserializer() { + return new MqttJsonDeserializer(); + } + + @Bean + @ConditionalOnMissingBean + public IMqttConnectStatusListener springEventMqttConnectStatusListener(ApplicationEventPublisher eventPublisher) { + return new SpringEventMqttConnectStatusListener(eventPublisher); + } + + @Bean + public MqttServerCreator mqttServerCreator(MqttServerProperties properties, + ObjectProvider authHandlerObjectProvider, + ObjectProvider uniqueIdServiceObjectProvider, + ObjectProvider subscribeValidatorObjectProvider, + ObjectProvider publishPermissionObjectProvider, + ObjectProvider messageDispatcherObjectProvider, + ObjectProvider messageStoreObjectProvider, + ObjectProvider sessionManagerObjectProvider, + ObjectProvider sessionListenerObjectProvider, + ObjectProvider messageListenerObjectProvider, + ObjectProvider connectStatusListenerObjectProvider, + ObjectProvider messageInterceptorObjectProvider, + ObjectProvider customizers) { + MqttServerCreator serverCreator = MqttServer.create() + .name(properties.getName()) + .heartbeatTimeout(properties.getHeartbeatTimeout()) + .keepaliveBackoff(properties.getKeepaliveBackoff()) + .readBufferSize((int) properties.getReadBufferSize().toBytes()) + .maxBytesInMessage((int) properties.getMaxBytesInMessage().toBytes()) + .maxClientIdLength(properties.getMaxClientIdLength()) + .nodeName(properties.getNodeName()) + .statEnable(properties.isStatEnable()) + .proxyProtocolEnable(properties.isProxyProtocolOn()); + if (properties.isDebug()) { + serverCreator.debug(); + } + // mqtt 协议 + MqttServerProperties.Listener mqttListener = properties.getMqttListener(); + if (mqttListener.isEnable()) { + serverCreator.enableMqtt(builder -> builder.serverNode(mqttListener.getServerNode()).build()); + } + // mqtt ssl 协议 + MqttServerProperties.SslListener mqttSslListener = properties.getMqttSslListener(); + if (mqttSslListener.isEnable()) { + MqttServerProperties.Ssl ssl = mqttSslListener.getSsl(); + serverCreator.enableMqttSsl(sslBuilder -> sslBuilder + .serverNode(mqttSslListener.getServerNode()) + .useSsl(ssl.getKeystorePath(), ssl.getKeystorePass(), ssl.getTruststorePath(), ssl.getTruststorePass(), ssl.getClientAuth()) + .build()); + } + // mqtt websocket 协议 + MqttServerProperties.Listener wsListener = properties.getWsListener(); + if (wsListener.isEnable()) { + serverCreator.enableMqttWs(builder -> builder.serverNode(wsListener.getServerNode()).build()); + } + MqttServerProperties.SslListener wssListener = properties.getWssListener(); + if (mqttSslListener.isEnable()) { + MqttServerProperties.Ssl ssl = wssListener.getSsl(); + serverCreator.enableMqttWss(sslBuilder -> sslBuilder + .serverNode(wssListener.getServerNode()) + .useSsl(ssl.getKeystorePath(), ssl.getKeystorePass(), ssl.getTruststorePath(), ssl.getTruststorePass(), ssl.getClientAuth()) + .build()); + } + // mqtt http api + MqttServerProperties.HttpListener httpListener = properties.getHttpListener(); + if (httpListener.isEnable()) { + Node serverNode = httpListener.getServerNode(); + MqttServerProperties.HttpBasicAuth basicAuth = httpListener.getBasicAuth(); + MqttServerProperties.McpServer mcpServer = httpListener.getMcpServer(); + MqttServerProperties.HttpSsl ssl = httpListener.getSsl(); + serverCreator.enableMqttHttpApi(builder -> { + builder.serverNode(serverNode); + if (basicAuth.isEnable()) { + builder.basicAuth(basicAuth.getUsername(), basicAuth.getPassword()); + } + if (mcpServer.isEnable()) { + builder.mcpServer(mcpServer.getSseEndpoint(), mcpServer.getMessageEndpoint()); + } + if (ssl.isEnable()) { + builder.useSsl(ssl.getKeystorePath(), ssl.getKeystorePass(), ssl.getTruststorePath(), ssl.getTruststorePass(), ssl.getClientAuth()); + } + return builder.build(); + }); + } + // 自定义消息监听 + messageListenerObjectProvider.ifAvailable(serverCreator::messageListener); + // 认证处理器 + IMqttServerAuthHandler authHandler = authHandlerObjectProvider.getIfAvailable(() -> { + MqttServerProperties.MqttAuth mqttAuth = properties.getAuth(); + return mqttAuth.isEnable() ? new DefaultMqttServerAuthHandler(mqttAuth.getUsername(), mqttAuth.getPassword()) : null; + }); + serverCreator.authHandler(authHandler); + // mqtt 内唯一id + uniqueIdServiceObjectProvider.ifAvailable(serverCreator::uniqueIdService); + // 订阅校验 + subscribeValidatorObjectProvider.ifAvailable(serverCreator::subscribeValidator); + // 订阅权限校验 + publishPermissionObjectProvider.ifAvailable(serverCreator::publishPermission); + // 消息转发 + messageDispatcherObjectProvider.ifAvailable(serverCreator::messageDispatcher); + // 消息存储 + messageStoreObjectProvider.ifAvailable(serverCreator::messageStore); + // session 管理 + sessionManagerObjectProvider.ifAvailable(serverCreator::sessionManager); + // session 监听 + sessionListenerObjectProvider.ifAvailable(serverCreator::sessionListener); + // 状态监听 + connectStatusListenerObjectProvider.ifAvailable(serverCreator::connectStatusListener); + // 消息监听器 + messageInterceptorObjectProvider.orderedStream().forEach(serverCreator::addInterceptor); + // 自定义处理 + customizers.ifAvailable((customizer) -> customizer.customize(serverCreator)); + return serverCreator; + } + + @Bean + public MqttServer mqttServer(MqttServerCreator mqttServerCreator) { + return mqttServerCreator.build(); + } + + @Bean + public MqttServerLauncher mqttServerLauncher(MqttServer mqttServer) { + return new MqttServerLauncher(mqttServer); + } + + @Bean + public MqttServerTemplate mqttServerTemplate(MqttServer mqttServer) { + return new MqttServerTemplate(mqttServer); + } + + @Bean + @ConditionalOnMissingBean(MqttFunctionManager.class) + public static MqttFunctionManager mqttFunctionManager() { + return new MqttFunctionManager(); + } + + @Bean + @ConditionalOnMissingBean(IMqttMessageListener.class) + public IMqttMessageListener mqttFunctionMessageListener(MqttFunctionManager mqttFunctionManager) { + return new MqttFunctionMessageListener(mqttFunctionManager); + } + + @Bean + public static MqttServerFunctionDetector mqttServerFunctionDetector(ApplicationContext applicationContext) { + return new MqttServerFunctionDetector(applicationContext); + } + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerLauncher.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerLauncher.java new file mode 100644 index 0000000..8141392 --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerLauncher.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.springframework.context.SmartLifecycle; +import org.springframework.core.Ordered; + +/** + * MqttServer 启动器 + * + * @author L.cm + */ +@Slf4j +@RequiredArgsConstructor +public class MqttServerLauncher implements SmartLifecycle, Ordered { + private final MqttServer mqttServer; + private volatile boolean running = false; + + @Override + public void start() { + running = mqttServer.start(); + } + + @Override + public void stop() { + mqttServer.stop(); + } + + @Override + public boolean isRunning() { + return running; + } + + @Override + public boolean isAutoStartup() { + return true; + } + + @Override + public void stop(Runnable callback) { + stop(); + callback.run(); + } + + @Override + public int getPhase() { + return DEFAULT_PHASE; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerMetrics.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerMetrics.java new file mode 100644 index 0000000..48bf4ab --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerMetrics.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server.config; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.tio.core.Tio; +import org.tio.server.ServerGroupStat; +import org.tio.server.TioServerConfig; + +import java.util.Collections; + +/** + * mica mqtt Metrics + * + * @author L.cm + */ +@Slf4j +@RequiredArgsConstructor +public class MqttServerMetrics implements ApplicationListener { + /** + * Prefix used for all mica-mqtt metric names. + */ + public static final String MQTT_METRIC_NAME_PREFIX = "mqtt"; + /** + * 连接统计 + */ + private static final String MQTT_CONNECTIONS_ACCEPTED = MQTT_METRIC_NAME_PREFIX + ".connections.accepted"; + private static final String MQTT_CONNECTIONS_SIZE = MQTT_METRIC_NAME_PREFIX + ".connections.size"; + private static final String MQTT_CONNECTIONS_CLOSED = MQTT_METRIC_NAME_PREFIX + ".connections.closed"; + /** + * 消息统计 + */ + private static final String MQTT_MESSAGES_HANDLED_PACKETS = MQTT_METRIC_NAME_PREFIX + ".messages.handled.packets"; + private static final String MQTT_MESSAGES_HANDLED_BYTES = MQTT_METRIC_NAME_PREFIX + ".messages.handled.bytes"; + private static final String MQTT_MESSAGES_RECEIVED_PACKETS = MQTT_METRIC_NAME_PREFIX + ".messages.received.packets"; + private static final String MQTT_MESSAGES_RECEIVED_BYTES = MQTT_METRIC_NAME_PREFIX + ".messages.received.bytes"; + private static final String MQTT_MESSAGES_SEND_PACKETS = MQTT_METRIC_NAME_PREFIX + ".messages.send.packets"; + private static final String MQTT_MESSAGES_SEND_BYTES = MQTT_METRIC_NAME_PREFIX + ".messages.send.bytes"; + + private final Iterable tags; + + public MqttServerMetrics() { + this(Collections.emptyList()); + } + + @Override + public void onApplicationEvent(ApplicationStartedEvent event) { + ApplicationContext applicationContext = event.getApplicationContext(); + MeterRegistry registry = getMeterRegistry(applicationContext); + if (registry != null) { + MqttServer mqttServer = applicationContext.getBean(MqttServer.class); + TioServerConfig serverConfig = mqttServer.getServerConfig(); + bindTo(registry, serverConfig); + } + } + + private MeterRegistry getMeterRegistry(ApplicationContext applicationContext) { + try { + return applicationContext.getBean(MeterRegistry.class); + } catch (NoSuchBeanDefinitionException e) { + log.warn(e.getMessage()); + return null; + } + } + + private void bindTo(MeterRegistry meterRegistry, TioServerConfig serverConfig) { + // 连接统计 + Gauge.builder(MQTT_CONNECTIONS_ACCEPTED, serverConfig, (config) -> ((ServerGroupStat) config.getGroupStat()).accepted.sum()) + .description("Mqtt server connections accepted") + .tags(tags) + .register(meterRegistry); + Gauge.builder(MQTT_CONNECTIONS_SIZE, serverConfig, (config) -> Tio.getAll(config).size()) + .description("Mqtt server connections size") + .tags(tags) + .register(meterRegistry); + Gauge.builder(MQTT_CONNECTIONS_CLOSED, serverConfig, (config) -> config.getGroupStat().getClosed().sum()) + .description("Mqtt server connections closed") + .tags(tags) + .register(meterRegistry); + // 消息统计 + Gauge.builder(MQTT_MESSAGES_HANDLED_PACKETS, serverConfig, (config) -> config.getGroupStat().getHandledPackets().sum()) + .description("Mqtt server handled packets") + .tags(tags) + .register(meterRegistry); + Gauge.builder(MQTT_MESSAGES_HANDLED_BYTES, serverConfig, (config) -> config.getGroupStat().getHandledBytes().sum()) + .description("Mqtt server handled bytes") + .tags(tags) + .register(meterRegistry); + // 接收的消息 + Gauge.builder(MQTT_MESSAGES_RECEIVED_PACKETS, serverConfig, (config) -> config.getGroupStat().getReceivedPackets().sum()) + .description("Mqtt server received packets") + .tags(tags) + .register(meterRegistry); + Gauge.builder(MQTT_MESSAGES_RECEIVED_BYTES, serverConfig, (config) -> config.getGroupStat().getReceivedBytes().sum()) + .description("Mqtt server received bytes") + .tags(tags) + .register(meterRegistry); + // 发送的消息 + Gauge.builder(MQTT_MESSAGES_SEND_PACKETS, serverConfig, (config) -> config.getGroupStat().getSentPackets().sum()) + .description("Mqtt server send packets") + .tags(tags) + .register(meterRegistry); + Gauge.builder(MQTT_MESSAGES_SEND_BYTES, serverConfig, (config) -> config.getGroupStat().getSentPackets().sum()) + .description("Mqtt server send bytes") + .tags(tags) + .register(meterRegistry); + } + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerMetricsConfiguration.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerMetricsConfiguration.java new file mode 100644 index 0000000..dcc0aa7 --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerMetricsConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server.config; + +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +/** + * mica mqtt Metrics 配置 + * + * @author L.cm + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty( + prefix = MqttServerProperties.PREFIX, + name = "enabled", + havingValue = "true" +) +@ConditionalOnClass(MeterRegistry.class) +@AutoConfigureAfter(MqttServerConfiguration.class) +public class MqttServerMetricsConfiguration { + + @Bean + public MqttServerMetrics micaMqttMetrics() { + return new MqttServerMetrics(); + } + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerProperties.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerProperties.java new file mode 100644 index 0000000..0d45de7 --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerProperties.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server.config; + +import lombok.Getter; +import lombok.Setter; +import org.dromara.mica.mqtt.codec.MqttConstant; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.unit.DataSize; +import org.tio.core.Node; +import org.tio.core.ssl.ClientAuth; + +/** + * MqttServer 配置 + * + * @author L.cm + */ +@Getter +@Setter +@ConfigurationProperties(MqttServerProperties.PREFIX) +public class MqttServerProperties { + + /** + * 配置前缀 + */ + public static final String PREFIX = "mqtt.server"; + /** + * 是否启用,默认:启用 + */ + private boolean enabled = true; + /** + * 名称 + */ + private String name = "Mica-Mqtt-Server"; + /** + * mqtt 认证 + */ + private MqttAuth auth = new MqttAuth(); + /** + * 心跳超时时间(单位: 毫秒 默认: 1000 * 120),如果用户不希望框架层面做心跳相关工作,请把此值设为0或负数 + */ + private Long heartbeatTimeout; + /** + * MQTT 客户端 keepalive 系数,连接超时缺省为连接设置的 keepalive * keepaliveBackoff * 2,默认:0.75 + *

+ * 如果读者想对该值做一些调整,可以在此进行配置。比如设置为 0.75,则变为 keepalive * 1.5。但是该值不得小于 0.5,否则将小于 keepalive 设定的时间。 + */ + private float keepaliveBackoff = 0.75F; + /** + * 接收数据的 buffer size,默认:8k + */ + private DataSize readBufferSize = DataSize.ofBytes(MqttConstant.DEFAULT_MAX_READ_BUFFER_SIZE); + /** + * 消息解析最大 bytes 长度,默认:10M + */ + private DataSize maxBytesInMessage = DataSize.ofBytes(MqttConstant.DEFAULT_MAX_BYTES_IN_MESSAGE); + /** + * debug + */ + private boolean debug = false; + /** + * mqtt 3.1 会校验此参数为 23,为了减少问题设置成了 64 + */ + private int maxClientIdLength = MqttConstant.DEFAULT_MAX_CLIENT_ID_LENGTH; + /** + * 节点名称,用于处理集群 + */ + private String nodeName; + /** + * 是否开启监控,不开启可节省内存,默认:true + */ + private boolean statEnable = true; + /** + * 开启代理协议,支持 nginx proxy_protocol on; + */ + private boolean proxyProtocolOn = false; + /** + * mqtt tcp 监听器 + */ + private Listener mqttListener = new Listener(); + /** + * mqtt tcp ssl 监听器 + */ + private SslListener mqttSslListener = new SslListener(); + /** + * websocket mqtt 监听器 + */ + private Listener wsListener = new Listener(); + /** + * websocket ssl mqtt 监听器 + */ + private SslListener wssListener = new SslListener(); + /** + * http api 监听器 + */ + private HttpListener httpListener = new HttpListener(); + + @Getter + @Setter + public static class MqttAuth { + /** + * 是否启用,默认:关闭 + */ + private boolean enable = false; + /** + * http Basic 认证账号 + */ + private String username; + /** + * http Basic 认证密码 + */ + private String password; + } + + @Getter + @Setter + public static class Listener { + /** + * 是否启用,默认:关闭 + */ + private boolean enable = false; + /** + * 服务端 ip + */ + private String ip; + /** + * 端口 + */ + private Integer port; + /** + * 获取服务节点 + * + * @return ServerNode + */ + public Node getServerNode() { + if (this.ip == null && this.port == null) { + return null; + } else { + return new Node(this.ip, this.port); + } + } + } + + @Getter + @Setter + public static class SslListener extends Listener { + /** + * ssl 配置 + */ + private Ssl ssl = new Ssl(); + } + + @Getter + @Setter + public static class Ssl { + /** + * keystore 证书路径 + */ + private String keystorePath; + /** + * keystore 密码 + */ + private String keystorePass; + /** + * truststore 证书路径 + */ + private String truststorePath; + /** + * truststore 密码 + */ + private String truststorePass; + /** + * 认证类型 + */ + private ClientAuth clientAuth = ClientAuth.NONE; + } + + @Getter + @Setter + public static class HttpListener extends Listener { + /** + * basic 认证 + */ + private HttpBasicAuth basicAuth = new HttpBasicAuth(); + /** + * mcp 配置 + */ + private McpServer mcpServer = new McpServer(); + /** + * ssl 配置 + */ + private HttpSsl ssl = new HttpSsl(); + } + + @Getter + @Setter + public static class HttpSsl extends Ssl { + /** + * 是否启用,默认:关闭 + */ + private boolean enable = false; + } + + @Getter + @Setter + public static class HttpBasicAuth { + /** + * 是否启用,默认:关闭 + */ + private boolean enable = false; + /** + * http Basic 认证账号 + */ + private String username; + /** + * http Basic 认证密码 + */ + private String password; + } + + @Getter + @Setter + public static class McpServer { + /** + * 是否启用,默认:关闭 + */ + private boolean enable = false; + /** + * sse 端点 + */ + private String sseEndpoint; + /** + * message 端点 + */ + private String messageEndpoint; + } +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/MqttClientOfflineEvent.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/MqttClientOfflineEvent.java new file mode 100644 index 0000000..378c578 --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/MqttClientOfflineEvent.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server.event; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 客户端断开原因 + * + * @author L.cm + */ +@Data +public class MqttClientOfflineEvent implements Serializable { + + /** + * 客户端 Id + */ + private String clientId; + /** + * 用户名 + */ + private String username; + /** + * 断开原因 + */ + private String reason; + /** + * 时间戳 + */ + private long ts; + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/MqttClientOnlineEvent.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/MqttClientOnlineEvent.java new file mode 100644 index 0000000..5335efb --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/MqttClientOnlineEvent.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server.event; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 客户端断开事件 + * + * @author L.cm + */ +@Data +public class MqttClientOnlineEvent implements Serializable { + + /** + * 客户端 id + */ + private String clientId; + /** + * 用户名 + */ + private String username; + /** + * ip + */ + private String ipAddress; + /** + * 端口 + */ + private int port; + /** + * keepalive + */ + private long keepalive; + /** + * 时间戳 + */ + private long ts; + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/SpringEventMqttConnectStatusListener.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/SpringEventMqttConnectStatusListener.java new file mode 100644 index 0000000..1f8c3ca --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/SpringEventMqttConnectStatusListener.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & dreamlu.net). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.mica.mqtt.spring.server.event; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.springframework.context.ApplicationEventPublisher; +import org.tio.core.ChannelContext; +import org.tio.core.Node; + +import java.util.concurrent.TimeUnit; + +/** + * spring event mqtt 连接状态 + * + * @author L.cm + */ +@Slf4j +@RequiredArgsConstructor +public class SpringEventMqttConnectStatusListener implements IMqttConnectStatusListener { + private final ApplicationEventPublisher eventPublisher; + + @Override + public void online(ChannelContext context, String clientId, String username) { + log.info("Mqtt clientId:{} username:{} online.", clientId, username); + MqttClientOnlineEvent onlineEvent = new MqttClientOnlineEvent(); + onlineEvent.setClientId(clientId); + onlineEvent.setUsername(username); + // clientNode + Node clientNode = context.getClientNode(); + onlineEvent.setIpAddress(clientNode.getIp()); + onlineEvent.setPort(clientNode.getPort()); + // keepalive + long keepalive = context.heartbeatTimeout == null ? 60L : TimeUnit.MILLISECONDS.toSeconds(context.heartbeatTimeout); + onlineEvent.setKeepalive(keepalive); + onlineEvent.setTs(context.stat.timeCreated); + eventPublisher.publishEvent(onlineEvent); + } + + @Override + public void offline(ChannelContext context, String clientId, String username, String reason) { + log.info("Mqtt clientId:{} username:{} offline reason:{}.", clientId, username, reason); + MqttClientOfflineEvent offlineEvent = new MqttClientOfflineEvent(); + offlineEvent.setClientId(clientId); + offlineEvent.setUsername(username); + offlineEvent.setReason(reason); + offlineEvent.setTs(context.stat.timeClosed); + eventPublisher.publishEvent(offlineEvent); + } + +} diff --git a/starter/mica-mqtt-server-spring-boot-starter/src/main/moditect/module-info.java b/starter/mica-mqtt-server-spring-boot-starter/src/main/moditect/module-info.java new file mode 100644 index 0000000..e8007ab --- /dev/null +++ b/starter/mica-mqtt-server-spring-boot-starter/src/main/moditect/module-info.java @@ -0,0 +1,11 @@ +open module org.dromara.mica.mqtt.server.spring.boot.starter { + requires lombok; + requires spring.core; + requires spring.context; + requires spring.boot; + requires spring.boot.autoconfigure; + requires transitive org.dromara.mica.mqtt.server; + exports org.dromara.mica.mqtt.spring.server; + exports org.dromara.mica.mqtt.spring.server.config; + exports org.dromara.mica.mqtt.spring.server.event; +} diff --git a/starter/pom.xml b/starter/pom.xml new file mode 100644 index 0000000..23f32bc --- /dev/null +++ b/starter/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + mica-mqtt + ${revision} + + starter + ${project.artifactId} + pom + + + mica-mqtt-client-spring-boot-starter + mica-mqtt-server-spring-boot-starter + mica-mqtt-client-solon-plugin + mica-mqtt-server-solon-plugin + mica-mqtt-client-jfinal-plugin + mica-mqtt-server-jfinal-plugin + + + + + + org.moditect + moditect-maven-plugin + + + + + diff --git a/system/README.md b/system/README.md new file mode 100644 index 0000000..26cb361 --- /dev/null +++ b/system/README.md @@ -0,0 +1,113 @@ +## 快速开始 + +**example** 中有 `mqtt` 服务端和客户端演示代码。 + +也可以只启动**Server**端,**Client**端用客户端工具测试。 + +### 1. 启动 Server 端 + +运行 `example/mica-mqtt-server-spring-boot-example/src/main/java/net/dreamlu/iot/mqtt/server/MqttServerApplication.java` 的 `main` 方法 + +控制台打印如下内容: + +```text +2021-07-05 20:42:36,869 INFO server.TioServer - +|----------------------------------------------------------------------------------------| +| t-io site | https://www.tiocloud.com | +| t-io on gitee | https://gitee.com/tywo45/t-io | +| t-io on github | https://github.com/tywo45/t-io | +| t-io version | 3.7.3.v20210706-RELEASE | +| ---------------------------------------------------------------------------------------| +| TioConfig name | Mica-Mqtt-Server | +| Started at | 2021-07-05 20:42:36 | +| Listen on | 127.0.0.1:1883 | +| Main Class | org.dromara.mica.mqtt.server.MqttServerTest | +| Jvm start time | 2715ms | +| Tio start time | 16ms | +| Pid | 3588 | +|----------------------------------------------------------------------------------------| + +2021-07-05 20:42:37,884 WARN server.MqttServer - Mqtt publish to all ChannelContext is empty. +``` + +`Mqtt publish to all ChannelContext is empty.` 通道上下文为空,即没有客户端。 + +```text +Mica-Mqtt-Server + ├ 当前时间:1625489086843 + ├ 连接统计 + │ ├ 共接受过连接数 :0 + │ ├ 当前连接数 :0 + │ ├ 异IP连接数 :0 + │ └ 关闭过的连接数 :0 + ├ 消息统计 + │ ├ 已处理消息 :0 + │ ├ 已接收消息(packet/byte):0/0 + │ ├ 已发送消息(packet/byte):0/0b + │ ├ 平均每次TCP包接收的字节数 :0.0 + │ └ 平均每次TCP包接收的业务包 :0.0 + └ IP统计时段 + └ 没有设置ip统计时间 + ├ 节点统计 + │ ├ clientNodes :0 + │ ├ 所有连接 :0 + │ ├ 绑定user数 :0 + │ ├ 绑定token数 :0 + │ └ 等待同步消息响应 :0 + ├ 群组 + │ └ groupmap:0 + └ 拉黑IP + └ [] +2021-07-05 20:44:46,925 WARN server.ServerTioConfig - Mica-Mqtt-Server, 检查心跳, 共0个连接, 取锁耗时0ms, 循环耗时71ms, 心跳超时时间:120000ms +``` + +### 2. 启动 Client 端 + +运行 `example/mica-mqtt-client-spring-boot-example/src/main/java/net/dreamlu/iot/mqtt/client/MqttClientApplication.java` 的 `main` 方法 + +控制台打印如下内容,表示客户端连接成功: +```text +2021-07-05 20:46:10,972 ERROR client.TioClient - closeds:0, connections:0 +2021-07-05 20:46:10,972 INFO client.TioClient - [1]: curr:0, closed:0, received:(0p)(0b), handled:0, sent:(0p)(0b) +2021-07-05 20:46:12,566 INFO client.ConnectionCompletionHandler - connected to 127.0.0.1:1883 +2021-07-05 20:46:12,586 INFO client.MqttClient - MqttClient reconnect send connect result:true +2021-07-05 20:46:12,630 INFO client.DefaultMqttClientProcessor - MqttClient connection succeeded! +2021-07-05 20:46:13,932 INFO client.MqttClientTest - /test/123 mica最牛皮 +``` + +此时的 Server 端会打印出如下内容: + +```text +2021-07-05 20:46:45,654 INFO server.MqttServerTest - subscribe: /test/client mica最牛皮 +2021-07-05 20:46:46,926 WARN server.ServerTioConfig - +Mica-Mqtt-Server + ├ 当前时间:1625489206923 + ├ 连接统计 + │ ├ 共接受过连接数 :1 + │ ├ 当前连接数 :1 + │ ├ 异IP连接数 :1 + │ └ 关闭过的连接数 :0 + ├ 消息统计 + │ ├ 已处理消息 :20 + │ ├ 已接收消息(packet/byte):20/584 + │ ├ 已发送消息(packet/byte):37/935b + │ ├ 平均每次TCP包接收的字节数 :29.2 + │ └ 平均每次TCP包接收的业务包 :1.0 + └ IP统计时段 + └ 没有设置ip统计时间 + ├ 节点统计 + │ ├ clientNodes :1 + │ ├ 所有连接 :1 + │ ├ 绑定user数 :0 + │ ├ 绑定token数 :0 + │ └ 等待同步消息响应 :0 + ├ 群组 + │ └ groupmap:0 + └ 拉黑IP + └ [] +2021-07-05 20:46:46,926 WARN server.ServerTioConfig - Mica-Mqtt-Server, 检查心跳, 共1个连接, 取锁耗时0ms, 循环耗时0ms, 心跳超时时间:120000ms +``` + +### 3. Client 接入 Aliyun MQTT 服务(示例) + +详见 `example/mica-mqtt-example/src/main/java/net/dreamlu/iot/mqtt/aliyun/MqttClientTest.java` diff --git a/system/mqtt-xf-receiver/README.md b/system/mqtt-xf-receiver/README.md new file mode 100644 index 0000000..9e80be2 --- /dev/null +++ b/system/mqtt-xf-receiver/README.md @@ -0,0 +1,14 @@ +## SpringBoot + mica-mqtt-server 应用演示 + +## 启动步骤 +1. 先启动 mica-mqtt-server-spring-boot-example + +2. 再启动 mica-mqtt-client-spring-boot-example + +3. 查看控制器 swagger 地址:http://localhost:30012/doc.html + +4. 可开启 prometheus 指标收集,详见: http://localhost:30012/actuator/prometheus + +## 连接 + +mica Spring boot 开发组件集文档:https://www.dreamlu.net/components/mica-swagger.html diff --git a/system/mqtt-xf-receiver/pom.xml b/system/mqtt-xf-receiver/pom.xml new file mode 100644 index 0000000..1adf12b --- /dev/null +++ b/system/mqtt-xf-receiver/pom.xml @@ -0,0 +1,130 @@ + + + 4.0.0 + mqtt-xf-receiver + + + org.dromara.mica-mqtt + system + ${revision} + + + + + + org.springframework.boot + spring-boot-dependencies + 4.0.0 + pom + import + + + + + + + org.dromara.mica-mqtt + mica-mqtt-server-spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + net.dreamlu + mica-lite + + + net.dreamlu + mica-logging + + + net.dreamlu + mica-openapi + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + org.projectlombok + lombok + provided + + + + com.baomidou + mybatis-plus-spring-boot4-starter + 3.5.14 + + + + + + + + + + mysql + mysql-connector-java + 8.0.33 + + + + com.alibaba + druid-spring-boot-starter + 1.2.20 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.23 + + + + + cn.hutool + hutool-all + 5.8.40 + + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + + diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/MqttServerApplication.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/MqttServerApplication.java new file mode 100644 index 0000000..e88448d --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/MqttServerApplication.java @@ -0,0 +1,23 @@ +package org.dromara.mica.mqtt.server; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * @author wsq + */ +@SpringBootApplication +@EnableScheduling +@MapperScan("org.dromara.mica.mqtt.server.mapper") +public class MqttServerApplication { + + /** + * 先启动本项目,再启动 mica-mqtt-client-spring-boot-example 进行测试 + */ + public static void main(String[] args) { + SpringApplication.run(MqttServerApplication.class, args); + } + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttAuthHandler.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttAuthHandler.java new file mode 100644 index 0000000..5c879b2 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttAuthHandler.java @@ -0,0 +1,40 @@ +package org.dromara.mica.mqtt.server.auth; + +import org.dromara.mica.mqtt.core.server.auth.IMqttServerAuthHandler; +import org.dromara.mica.mqtt.spring.server.config.MqttServerProperties; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.tio.core.ChannelContext; + +/** + * 示例 mqtt tcp、websocket 认证,请按照自己的需求和业务进行扩展 + * + * @author L.cm + */ +@Configuration(proxyBeanMethods = false) +public class MqttAuthHandler implements IMqttServerAuthHandler { + + @Value("${mqtt.server.auth.enable}") + private boolean enable; + + @Value("${mqtt.server.auth.username}") + private String username; + + @Value("${mqtt.server.auth.password}") + private String password; + + @Override + public boolean authenticate(ChannelContext context, String uniqueId, String clientId, String username, String password) { + // 客户端认证逻辑实现 + if (enable) { + if (username.equals(this.username) && password.equals(this.password)) { + return true; + } else { + return false; + } + } else { + return false; + } + } + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttHttpAuthFilter.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttHttpAuthFilter.java new file mode 100644 index 0000000..8491659 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttHttpAuthFilter.java @@ -0,0 +1,36 @@ +package org.dromara.mica.mqtt.server.auth; + +import org.dromara.mica.mqtt.core.server.http.api.code.ResultCode; +import org.dromara.mica.mqtt.core.server.http.api.result.Result; +import org.dromara.mica.mqtt.core.server.http.handler.HttpFilter; +import org.dromara.mica.mqtt.core.server.http.handler.MqttHttpRoutes; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.Configuration; +import org.tio.http.common.HttpRequest; +import org.tio.http.common.HttpResponse; + +/** + * 示例自定义 mqtt http 接口认证,请按照自己的需求和业务进行扩展 + * + * @author L.cm + */ +@Configuration(proxyBeanMethods = false) +public class MqttHttpAuthFilter implements HttpFilter, InitializingBean { + + @Override + public boolean filter(HttpRequest request) throws Exception { + // 自行实现逻辑 + return true; + } + + @Override + public HttpResponse response(HttpRequest request) { + // 认证不通过时的响应 + return Result.fail(request, ResultCode.E103); + } + + @Override + public void afterPropertiesSet() throws Exception { + MqttHttpRoutes.addFilter(this); + } +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttSubscribeValidator.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttSubscribeValidator.java new file mode 100644 index 0000000..e640605 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttSubscribeValidator.java @@ -0,0 +1,22 @@ +package org.dromara.mica.mqtt.server.auth; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.server.auth.IMqttServerSubscribeValidator; +import org.springframework.context.annotation.Configuration; +import org.tio.core.ChannelContext; + +/** + * 示例自定义订阅校验,请按照自己的需求和业务进行扩展 + * + * @author L.cm + */ +@Configuration(proxyBeanMethods = false) +public class MqttSubscribeValidator implements IMqttServerSubscribeValidator { + + @Override + public boolean isValid(ChannelContext context, String clientId, String topicFilter, MqttQoS qoS) { + // 校验客户端订阅的 topic,校验成功返回 true,失败返回 false + return true; + } + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttUniqueIdService.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttUniqueIdService.java new file mode 100644 index 0000000..347721b --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/auth/MqttUniqueIdService.java @@ -0,0 +1,21 @@ +package org.dromara.mica.mqtt.server.auth; + +import org.dromara.mica.mqtt.core.server.auth.IMqttServerUniqueIdService; +import org.springframework.context.annotation.Configuration; +import org.tio.core.ChannelContext; + +/** + * 示例自定义 clientId,请按照自己的需求和业务进行扩展 + * + * @author L.cm + */ +@Configuration(proxyBeanMethods = false) +public class MqttUniqueIdService implements IMqttServerUniqueIdService { + + @Override + public String getUniqueId(ChannelContext context, String clientId, String userName, String password) { + // 返回的 uniqueId 会替代 mqtt client 传过来的 clientId,请保证返回的 uniqueId 唯一。 + return clientId; + } + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/config/MybatisPlusConfig.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/config/MybatisPlusConfig.java new file mode 100644 index 0000000..891090b --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/config/MybatisPlusConfig.java @@ -0,0 +1,38 @@ +package org.dromara.mica.mqtt.server.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * mybatis-plus配置 + * + * @author Mark sunlightcs@gmail.com + */ +@Configuration +public class MybatisPlusConfig { + + /** + * 配置分页等 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); + // 乐观锁 + mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + + return mybatisPlusInterceptor; + } + +// @Bean +// public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { +// MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); +// sqlSessionFactoryBean.setDataSource(dataSource); +// sqlSessionFactoryBean.setMapperLocations( +// new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/**/*.xml") +// ); +// return sqlSessionFactoryBean.getObject(); +// } + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/config/RedisConfig.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/config/RedisConfig.java new file mode 100644 index 0000000..26aa9a2 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/config/RedisConfig.java @@ -0,0 +1,41 @@ +package org.dromara.mica.mqtt.server.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(factory); + //使用jackson进行序列化 + Jackson2JsonRedisSerializer jsonRedisSerializer = + new Jackson2JsonRedisSerializer(Object.class); + //规定序列化规则 + ObjectMapper objectMapper = new ObjectMapper(); + /** + * 第一个参数指的是序列化的域,ALL指的是字段、get和set方法、构造方法 + * 第二个参数指的是序列化哪些访问修饰符,默认是public,ANY指任何访问修饰符 + */ + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + //指定序列化输入的类型,类必须是非final修饰的类 + objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); + jsonRedisSerializer.setObjectMapper(objectMapper); + //序列化key value + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(jsonRedisSerializer); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(jsonRedisSerializer); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/constant/CacheConstants.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/constant/CacheConstants.java new file mode 100644 index 0000000..0e177fd --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/constant/CacheConstants.java @@ -0,0 +1,19 @@ +package org.dromara.mica.mqtt.server.constant; + +/** + * 缓存常量信息 + */ +public class CacheConstants +{ + + /** + * 设备心跳缓存key + */ + public static final String EQUIPMENT_HEARTBEAT = "equipment:heartbeat:"; + + /** + * 设备心跳缓存过期时间 + */ + public static final long OFFLINE_THRESHOLD = 10 * 1000; + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/controller/ServerController.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/controller/ServerController.java new file mode 100644 index 0000000..387af2d --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/controller/ServerController.java @@ -0,0 +1,29 @@ +package org.dromara.mica.mqtt.server.controller; + +import com.alibaba.fastjson2.JSONObject; +import org.dromara.mica.mqtt.server.service.impl.ServerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("/mqtt/server") +@RestController +public class ServerController { + @Autowired + private ServerService service; + + @PostMapping("publish") + public JSONObject publish(@RequestBody JSONObject js) { + boolean publish = service.publish(js); + JSONObject jsonObject = new JSONObject(); + if (publish) { + jsonObject.put("code", 200); + } else { + jsonObject.put("code", 500); + } + return jsonObject; + } + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/entity/Equipment.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/entity/Equipment.java new file mode 100644 index 0000000..f3a5bdf --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/entity/Equipment.java @@ -0,0 +1,48 @@ +package org.dromara.mica.mqtt.server.entity; + + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@TableName("sys_equipment") +public class Equipment implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + + /** 设备Id */ + private Long id; + + /** 所属产品Id */ + private Long productId; + + /** 设备名称 */ + private String name; + + /** 设备序列号 */ + private String sequence; + + /** 设备Ip */ + private String ip; + + /** 设备密码 */ + private String password; + + /** 设备区域 */ + private Long spaceId; + + /** 设备位置 */ + private Long pointId; + + /** 对接状态(0未对接 1对接成功) */ + private Long state; + + /** 设备状态(0在线 1离线) */ + private String flag; + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/enums/FlagEnums.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/enums/FlagEnums.java new file mode 100644 index 0000000..428d667 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/enums/FlagEnums.java @@ -0,0 +1,27 @@ +package org.dromara.mica.mqtt.server.enums; + +/** + * 设备是否在线 + * + */ +public enum FlagEnums { + ONLINE("0", "在线"), + OFFLINE("1", "离线") + ; + + FlagEnums(String code, String name) { + this.code = code; + this.name = name; + } + + private String code; + private String name; + + public String getCode() { + return code; + } + + public String getValue() { + return name; + } +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/CarMessageListener.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/CarMessageListener.java new file mode 100644 index 0000000..81d36c3 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/CarMessageListener.java @@ -0,0 +1,52 @@ +package org.dromara.mica.mqtt.server.listener; + +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.annotation.MqttServerFunction; +import org.dromara.mica.mqtt.server.constant.CacheConstants; +import org.dromara.mica.mqtt.server.enums.FlagEnums; +import org.dromara.mica.mqtt.server.pojo.WhiteListOperatorPO; +import org.dromara.mica.mqtt.server.redis.RedisService; +import org.dromara.mica.mqtt.server.utils.AESUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.tio.utils.hutool.StrUtil; + +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 消息监听器 + * + * @author wsq + */ +@Slf4j +@Service +public class CarMessageListener { + + @Autowired + RedisService redisService; + + /** + * 心跳 + * @param topic + * @param topicVars + * @param message + */ + @MqttServerFunction("${sn}") + public void onKeepAliveMessage(String topic, Map topicVars, byte[] message) { + String sn = topicVars.get("sn"); + log.info("接收到来自客户端 [{}] 的心跳消息 -> Topic: {}, TopicVars: {}, Message: {}", sn, topic,topicVars,new String(message, StandardCharsets.UTF_8)); + + // 更新客户端的最后心跳 + redisService.setCacheObject(CacheConstants.EQUIPMENT_HEARTBEAT + sn, FlagEnums.ONLINE.getCode(), CacheConstants.OFFLINE_THRESHOLD, TimeUnit.MILLISECONDS); + } + + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener1.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener1.java new file mode 100644 index 0000000..f651c54 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener1.java @@ -0,0 +1,27 @@ +package org.dromara.mica.mqtt.server.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.spring.server.event.MqttClientOfflineEvent; +import org.dromara.mica.mqtt.spring.server.event.MqttClientOnlineEvent; +import org.springframework.context.event.EventListener; + +/** + * mqtt 连接状态,使用 spring boot event 方式,性能有损耗 + * + * @author L.cm + */ +@Slf4j +//@Service +public class MqttConnectStatusListener1 { + + @EventListener + public void online(MqttClientOnlineEvent event) { + log.info("MqttClientOnlineEvent:{}", event); + } + + @EventListener + public void offline(MqttClientOfflineEvent event) { + log.info("MqttClientOfflineEvent:{}", event); + } + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener2.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener2.java new file mode 100644 index 0000000..96eed79 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener2.java @@ -0,0 +1,43 @@ +package org.dromara.mica.mqtt.server.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.dromara.mica.mqtt.server.constant.CacheConstants; +import org.dromara.mica.mqtt.server.enums.FlagEnums; +import org.dromara.mica.mqtt.server.redis.RedisService; +import org.dromara.mica.mqtt.server.service.IEquipmentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.tio.core.ChannelContext; + +/** + * mqtt 连接状态 + * + * @author L.cm + */ +@Slf4j +@Service +public class MqttConnectStatusListener2 implements IMqttConnectStatusListener { + + @Autowired + IEquipmentService equipmentService; + + @Autowired + RedisService redisService; + + @Override + public void online(ChannelContext context, String clientId, String username) { + //设备上线不做任何处理,只有心跳报文做处理 + log.info("online-context: {}", context); + log.info("设备:{}上线", clientId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void offline(ChannelContext context, String clientId, String username, String reason) { + log.info("offline-context: {}", context); + redisService.deleteObject(CacheConstants.EQUIPMENT_HEARTBEAT + clientId); + log.info("设备:{}离线,offline reason:{}.", clientId, reason); + } +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/MqttServerMessageListener1.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/MqttServerMessageListener1.java new file mode 100644 index 0000000..9e32d18 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/listener/MqttServerMessageListener1.java @@ -0,0 +1,37 @@ +package org.dromara.mica.mqtt.server.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.server.event.IMqttMessageListener; +import org.dromara.mica.mqtt.spring.server.MqttServerTemplate; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.tio.core.ChannelContext; + +import java.nio.charset.StandardCharsets; + +/** + * 消息监听器示例1,直接实现 IMqttMessageListener,注意:如果实现了 IMqttMessageListener,MqttServerFunction 注解就不生效了。 + * + * @author wsq + */ +@Slf4j +public class MqttServerMessageListener1 implements IMqttMessageListener, SmartInitializingSingleton { + @Autowired + private ApplicationContext applicationContext; + + private MqttServerTemplate mqttServerTemplate; + + @Override + public void onMessage(ChannelContext context, String clientId, String topic, MqttQoS qos, MqttPublishMessage message) { + log.info("context:{} clientId:{} message:{} payload:{}", context, clientId, message, new String(message.payload(), StandardCharsets.UTF_8)); + } + + @Override + public void afterSingletonsInstantiated() { + // 单利 bean 初始化完成之后从 ApplicationContext 中获取 bean + mqttServerTemplate = applicationContext.getBean(MqttServerTemplate.class); + } +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/mapper/EquipmentMapper.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/mapper/EquipmentMapper.java new file mode 100644 index 0000000..a9fc363 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/mapper/EquipmentMapper.java @@ -0,0 +1,10 @@ +package org.dromara.mica.mqtt.server.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.dromara.mica.mqtt.server.entity.Equipment; + +@Mapper +public interface EquipmentMapper extends BaseMapper { + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/pojo/WhiteListOperatorPO.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/pojo/WhiteListOperatorPO.java new file mode 100644 index 0000000..2b17951 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/pojo/WhiteListOperatorPO.java @@ -0,0 +1,47 @@ +package org.dromara.mica.mqtt.server.pojo; +import com.alibaba.fastjson2.JSONObject; +import lombok.Data; + +/** + * 设备报文数据实体 + */ +@Data +public class WhiteListOperatorPO { + + /** + * id + */ + private String id; + + /** + * 回执的code + */ + private Integer code; + + /** + * 设备编码 + */ + private String sn; + + /** + * 报文名称 + */ + private String name; + + /** + * 版本 + */ + private String version = "1.0"; + + /** + * 内容 + */ + private JSONObject payload; + + /** + * 时间戳(精确到秒) + */ + private Long timestamp = System.currentTimeMillis() / 1000; + + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/redis/RedisService.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/redis/RedisService.java new file mode 100644 index 0000000..09b746d --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/redis/RedisService.java @@ -0,0 +1,302 @@ +package org.dromara.mica.mqtt.server.redis; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.BoundSetOperations; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Component +@Slf4j +public class RedisService { + + + @Autowired + public RedisTemplate redisTemplate; + + private static final long DEFAULT_TIMEOUT = 60 * 60 * 24 * 7; + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) + { + redisTemplate.opsForValue().set(key, value); + } + + + /** + * 缓存是否存在,存在返回false,不存在返回true并存储缓存值 + * + * @param key + * @param value + */ + public Boolean setIfAbsent(final String key, final String value, long timeout, TimeUnit timeUnit) + { + try { + return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, timeUnit); + } catch (Exception e) { + log.error("redis error:{}", e.getMessage()); + return false; + } + } + + /** + * 分布式加锁 + * + * @param key + * @param value + */ + public Boolean lock(final String key, final String value, long timeout, TimeUnit timeUnit) + { + try { + if (timeout <= 0) { + timeout = DEFAULT_TIMEOUT; + timeUnit = TimeUnit.SECONDS; + } + return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, timeUnit); + } catch (Exception e) { + log.error("redis error:{}", e.getMessage()); + return false; + } + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit) + { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) + { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) + { + return redisTemplate.expire(key, timeout, unit); + } + + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) + { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) + { + return redisTemplate.hasKey(key); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) + { + ValueOperations operation = redisTemplate.opsForValue(); + return operation.get(key); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) + { + return redisTemplate.delete(key); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public boolean deleteObject(final Collection collection) + { + return redisTemplate.delete(collection) > 0; + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long setCacheList(final String key, final List dataList) + { + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) + { + return redisTemplate.opsForList().range(key, 0, -1); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public BoundSetOperations setCacheSet(final String key, final Set dataSet) + { + BoundSetOperations setOperation = redisTemplate.boundSetOps(key); + Iterator it = dataSet.iterator(); + while (it.hasNext()) + { + setOperation.add(it.next()); + } + return setOperation; + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) + { + return redisTemplate.opsForSet().members(key); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) + { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) + { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) + { + redisTemplate.opsForHash().put(key, hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) + { + HashOperations opsForHash = redisTemplate.opsForHash(); + return opsForHash.get(key, hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) + { + return redisTemplate.opsForHash().multiGet(key, hKeys); + } + + /** + * 删除Hash中的某条数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return 是否成功 + */ + public boolean deleteCacheMapValue(final String key, final String hKey) + { + return redisTemplate.opsForHash().delete(key, hKey) > 0; + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) + { + return redisTemplate.keys(pattern); + } +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/service/IEquipmentService.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/service/IEquipmentService.java new file mode 100644 index 0000000..04ee0b4 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/service/IEquipmentService.java @@ -0,0 +1,15 @@ +package org.dromara.mica.mqtt.server.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.dromara.mica.mqtt.server.entity.Equipment; + +import java.util.List; + +public interface IEquipmentService extends IService { + + Equipment selectEquipmentBySn(String sn); + + List selectAllSnFlag(); + + void updateFlag(String sn, String flag); +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/service/impl/EquipmentServiceImpl.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/service/impl/EquipmentServiceImpl.java new file mode 100644 index 0000000..286d8c4 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/service/impl/EquipmentServiceImpl.java @@ -0,0 +1,35 @@ +package org.dromara.mica.mqtt.server.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.dromara.mica.mqtt.server.entity.Equipment; +import org.dromara.mica.mqtt.server.mapper.EquipmentMapper; +import org.dromara.mica.mqtt.server.service.IEquipmentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class EquipmentServiceImpl extends ServiceImpl implements IEquipmentService { + + @Autowired + EquipmentMapper equipmentMapper; + + @Override + public Equipment selectEquipmentBySn(String sn) { + return equipmentMapper.selectOne(new QueryWrapper().eq("sequence", sn).eq("product_id", 4L).last("limit 1")); + } + + @Override + public List selectAllSnFlag() { + return equipmentMapper.selectList(new QueryWrapper().eq("product_id", 4L).select("sequence", "flag")); + } + + @Override + public void updateFlag(String sn, String flag) { + Equipment equipment = new Equipment(); + equipment.setFlag(flag); + equipmentMapper.update(equipment, new QueryWrapper().eq("sequence", sn).eq("product_id", 4L)); + } +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/service/impl/ServerService.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/service/impl/ServerService.java new file mode 100644 index 0000000..56490e5 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/service/impl/ServerService.java @@ -0,0 +1,29 @@ +package org.dromara.mica.mqtt.server.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.spring.server.MqttServerTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; + +/** + * @author wsq + */ +@Slf4j +@Service +public class ServerService { + @Autowired + private MqttServerTemplate server; + + public boolean publish(JSONObject js) { + String sn = js.getString("sn"); + String topic = js.getString("topic"); + String body = js.getString("body"); + boolean result = server.publish(sn,topic, body.getBytes(StandardCharsets.UTF_8)); + log.info("publish-topic:{},body:{},result:{}", topic, body, result); + return result; + } + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/task/HeartbeatOnLineTask.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/task/HeartbeatOnLineTask.java new file mode 100644 index 0000000..4c3e6d1 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/task/HeartbeatOnLineTask.java @@ -0,0 +1,44 @@ +package org.dromara.mica.mqtt.server.task; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.server.constant.CacheConstants; +import org.dromara.mica.mqtt.server.entity.Equipment; +import org.dromara.mica.mqtt.server.enums.FlagEnums; +import org.dromara.mica.mqtt.server.mapper.EquipmentMapper; +import org.dromara.mica.mqtt.server.redis.RedisService; +import org.dromara.mica.mqtt.server.service.IEquipmentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.tio.utils.hutool.StrUtil; + +import java.util.List; + +/** + * 检查设备心跳,判断设备是否在线 + */ +@Slf4j +@Service +public class HeartbeatOnLineTask { + + @Autowired + RedisService redisService; + + @Autowired + IEquipmentService equipmentService; + + @Scheduled(fixedRate = 10 * 1000) + public void run() { + log.info("===========心跳检测============="); + //缓存中有该设备心跳key + if (redisService.hasKey(CacheConstants.EQUIPMENT_HEARTBEAT)) { + + } else { + //没有心跳上传,且设备在线,将设备置为离线 + + } + + + } +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/AESUtil.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/AESUtil.java new file mode 100644 index 0000000..32d7355 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/AESUtil.java @@ -0,0 +1,217 @@ +package org.dromara.mica.mqtt.server.utils; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Random; + +/** + * AES加密工具类 + */ +public class AESUtil { + + + /** + * 加密Key 需要16位 可用数字与字母组成 + */ + private static String key = "1234567898765432"; + /** + * 偏移量 需要16位 + */ + private static String iv = "4w2Df1xSj5ff662d"; + + private static Logger log = LoggerFactory.getLogger(AESUtil.class); + + private static Base64.Decoder decoder; + + private static Base64.Encoder encoder; + + + + static { + decoder = Base64.getDecoder(); + encoder = Base64.getEncoder(); + } + + + + public static String getSixteenBitString(){ + StringBuffer sb = new StringBuffer(); + String[] chars = new String[]{ + "1","2","3","4","5","6","7","8","9","a","b", + "c","d","e","f","g","h","i","j","k","l","m", + "n","o","p","q","r","s","t","u","v","w","x", + "y","z","A","B","C","D","E","F","G","H","I", + "J","K","L","M","N","O","P","Q","R","S","T", + "U","V","W","X","Y","Z", + }; + int len = chars.length; + Random random = new Random(); + for (int i = 0; i < 16; i++) { + sb.append(chars[random.nextInt(len-1)]); + } + return sb.toString(); + + } + + + + /** + * AES加密 + * @param data + * @param key + * @param iv + * @return + * @throws Exception + */ + public static String encryptAES_CBC(String data,String key,String iv) { + Cipher cipher = null; + try { + cipher = Cipher.getInstance("AES/CBC/NoPadding"); + + int blockSize = cipher.getBlockSize(); + byte[] dataBytes = data.getBytes(); + int dataLength = dataBytes.length; + if (dataLength % blockSize != 0) { + dataLength = dataLength + (blockSize - (dataLength % blockSize)); + } + byte[] plaintext = new byte[dataLength]; + System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length); + SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES"); + IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes()); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec); + byte[] bytes = cipher.doFinal(plaintext); + return encoder.encodeToString(bytes); + + }catch (Exception e) { + log.error("AES加密失败"); + log.error(e.getMessage()); + + } + + return null; + + } + /** + * AES解密 + * @param data + * @param key + * @param iv + * @return + * @throws Exception + */ + public static String decrptyAES_CBC(String data,String key,String iv){ + try { + byte[] bytes = decoder.decode(data); + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES"); + IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes()); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); + byte[] plainByte = cipher.doFinal(bytes); + return new String(plainByte).trim(); + }catch (Exception e){ + log.error("AES解密失败"); + log.error(e.getMessage()); + } + + return null; + } + + + /** + * AES加密 + * @param data + * @return + * @throws Exception + */ + public static String encryptAES_ECB(String data,String key) throws Exception{ + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + int blockSize = cipher.getBlockSize(); + byte[] dataBytes = data.getBytes(); + int dataLength = dataBytes.length; + if(dataLength % blockSize != 0){ + dataLength = dataLength + (blockSize - (dataLength % blockSize)); + } + byte [] plaintext = new byte[dataLength]; + System.arraycopy(dataBytes,0,plaintext,0,dataBytes.length); + SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(),"AES"); + cipher.init(Cipher.ENCRYPT_MODE,secretKey); + byte[] bytes = cipher.doFinal(plaintext); + return encoder.encodeToString(bytes); + + + } + + /** + * AES解密 + * @param data + * @return + * @throws Exception + */ + public static String decrptyAES_ECB(String data,String key) throws Exception{ + // byte[] bytes = decoder.decode(data); + byte[] bytes =Base64.getDecoder().decode(data); + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(),"AES"); + cipher.init(Cipher.DECRYPT_MODE,secretKey); + byte[] plainByte = cipher.doFinal(bytes); + return bytesToHex(plainByte); + } + private static String bytesToHex(byte[] bytes) { + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + return hexString.toString(); + } + + + /** + * gb2312编码 + */ + public static String gb2312decode( String string) throws UnsupportedEncodingException{ + byte[] bytes = new byte[string.length() / 2]; + for(int i = 0; i < bytes.length; i ++){ + byte high = Byte.parseByte(string.substring(i * 2, i * 2 + 1), 16); + byte low = Byte.parseByte(string.substring(i * 2 + 1, i * 2 + 2), 16); + bytes[i] = (byte) (high << 4 | low); + } + return new String(bytes, "gb2312"); + } + + + /** + * UTF8编码 + */ + public static String UTF8decode( String string) throws UnsupportedEncodingException{ + byte[] bytes = new byte[string.length() / 2]; + for(int i = 0; i < bytes.length; i ++){ + byte high = Byte.parseByte(string.substring(i * 2, i * 2 + 1), 16); + byte low = Byte.parseByte(string.substring(i * 2 + 1, i * 2 + 2), 16); + bytes[i] = (byte) (high << 4 | low); + } + return new String(bytes, StandardCharsets.UTF_8); + } + + + public static void main(String[] args) throws Exception{ + String encrypt = "IcDSUR8fdtJ8gLYlZX9qLw=="; + String str = decrptyAES_ECB(encrypt, key); +// String str1 = str.substring(0, 16); + String license = AESUtil.UTF8decode(str); +// System.out.println(gb2312decode(str1)); + System.out.println(license); + } + + + +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/Base64MultipartFile.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/Base64MultipartFile.java new file mode 100644 index 0000000..ad9d1f2 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/Base64MultipartFile.java @@ -0,0 +1,75 @@ +package org.dromara.mica.mqtt.server.utils; +import org.springframework.web.multipart.MultipartFile; +import java.io.*; +import java.util.Base64; + +public class Base64MultipartFile implements MultipartFile { + + private final byte[] fileContent; + private final String fileName; + private final String contentType; + + public Base64MultipartFile(String base64Data, String fileName) { + // 清理Base64数据 + String cleanedBase64 = base64Data.contains(",") + ? base64Data.substring(base64Data.indexOf(",") + 1) + : base64Data; + + this.fileContent = Base64.getDecoder().decode(cleanedBase64); + this.fileName = fileName; + this.contentType = extractContentType(base64Data); + } + + private String extractContentType(String base64Data) { + if (base64Data.startsWith("data:image/jpeg")) { + return "image/jpeg"; + } else if (base64Data.startsWith("data:image/png")) { + return "image/png"; + } else if (base64Data.startsWith("data:image/gif")) { + return "image/gif"; + } + return "application/octet-stream"; + } + + @Override + public String getName() { + return "file"; + } + + @Override + public String getOriginalFilename() { + return fileName; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public boolean isEmpty() { + return fileContent == null || fileContent.length == 0; + } + + @Override + public long getSize() { + return fileContent.length; + } + + @Override + public byte[] getBytes() throws IOException { + return fileContent; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(fileContent); + } + + @Override + public void transferTo(File dest) throws IOException, IllegalStateException { + try (FileOutputStream fos = new FileOutputStream(dest)) { + fos.write(fileContent); + } + } +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/Base64ToMultipartFileUtil.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/Base64ToMultipartFileUtil.java new file mode 100644 index 0000000..b7b7976 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/Base64ToMultipartFileUtil.java @@ -0,0 +1,28 @@ +package org.dromara.mica.mqtt.server.utils; + +import org.springframework.web.multipart.MultipartFile; + +public class Base64ToMultipartFileUtil { + + public static MultipartFile convertToMultipartFile(String base64Data, String fileName) { + return new Base64MultipartFile(base64Data, fileName); + } + + /** + * 自动生成文件名 + */ + public static MultipartFile convertToMultipartFile(String base64Data) { + String fileName = generateFileName(base64Data); + return convertToMultipartFile(base64Data, fileName); + } + + private static String generateFileName(String base64Data) { + String extension = ".jpg"; + if (base64Data.startsWith("data:image/png")) { + extension = ".png"; + } else if (base64Data.startsWith("data:image/gif")) { + extension = ".gif"; + } + return "image_" + System.currentTimeMillis() + extension; + } +} diff --git a/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/UuidUtil.java b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/UuidUtil.java new file mode 100644 index 0000000..251b57f --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/java/org/dromara/mica/mqtt/server/utils/UuidUtil.java @@ -0,0 +1,40 @@ +package org.dromara.mica.mqtt.server.utils; + +import java.util.UUID; + +public class UuidUtil { + + public static String[] chars = new String[] { "a", "b", "c", "d", "e", "f", + "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", + "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", + "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", + "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z" }; + + + /** + * 获取短UUID + * @return + */ + public static String getShortUuid() { + StringBuffer shortBuffer = new StringBuffer(); + String uuid = UuidUtil.getUuid(); + for (int i = 0; i < 8; i++) { + String str = uuid.substring(i * 4, i * 4 + 4); + int x = Integer.parseInt(str, 16); + shortBuffer.append(chars[x % 0x3E]); // 对62取余 + } + return shortBuffer.toString(); + + } + + /** + * 获得32位UUID + */ + public static String getUuid(){ + String uuid = UUID.randomUUID().toString(); + //去掉“-”符号 + return uuid.replaceAll("-", ""); + } + +} diff --git a/system/mqtt-xf-receiver/src/main/resources/application-dev.yml b/system/mqtt-xf-receiver/src/main/resources/application-dev.yml new file mode 100644 index 0000000..9c971ba --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/resources/application-dev.yml @@ -0,0 +1,12 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/xa_cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: root + data: + redis: + host: 192.168.2.30 + password: redis2025 + database: 5 + port: 6379 diff --git a/system/mqtt-xf-receiver/src/main/resources/application-prod.yml b/system/mqtt-xf-receiver/src/main/resources/application-prod.yml new file mode 100644 index 0000000..aba8da4 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/resources/application-prod.yml @@ -0,0 +1,28 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + #xa +# url: jdbc:mysql://127.0.0.1:3306/xa_cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 +# username: root +# password: Xahg2024. + #jl +# url: jdbc:mysql://127.0.0.1:3306/jl_cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 +# username: root +# password: JL202509jj + #td +# url: jdbc:mysql://127.0.0.1:3306/td_cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 +# username: root +# password: td@JJ2024 + #zr + url: jdbc:mysql://192.168.155.42:3306/zr_cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: zr202407.J + data: + redis: + #zr + host: 192.168.155.42 + #xa、jl、td +# host: 127.0.0.1 + port: 6379 + password: + database: 1 diff --git a/system/mqtt-xf-receiver/src/main/resources/application.yml b/system/mqtt-xf-receiver/src/main/resources/application.yml new file mode 100644 index 0000000..4ad0c08 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/resources/application.yml @@ -0,0 +1,129 @@ +server: + port: 30013 + +spring: + application: + name: mica-mqtt-server + # 环境 dev|test|prod + profiles: + active: dev +# active: prod + messages: + encoding: UTF-8 + basename: i18n/messages + # jackson时间格式化 + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + mvc: + pathmatch: + matching-strategy: ANT_PATH_MATCHER + servlet: + multipart: + max-file-size: 100MB + max-request-size: 100MB + enabled: true + +#mybatis +#mybatis-plus: +# mapper-locations: classpath*:/mapper/**/*.xml +# #实体扫描,多个package用逗号或者分号分隔 +# typeAliasesPackage: io.renren.entity +# global-config: +# #数据库相关配置 +# db-config: +# #主键类型 +# id-type: ASSIGN_ID +# banner: false +# #原生配置 +# configuration: +# map-underscore-to-camel-case: true +# cache-enabled: false +# call-setters-on-nulls: true +# jdbc-type-for-null: 'null' +# configuration-properties: +# prefix: +# blobType: BLOB +# boolValue: TRUE + + + +# mqtt 服务端配置 +mqtt: + server: + enabled: true # 是否开启服务端,默认:true + name: Mica-Mqtt-Server # 名称,默认:Mica-Mqtt-Server + heartbeat-timeout: 120000 # 心跳超时,单位毫秒,默认: 1000 * 120 + read-buffer-size: 8KB # 接收数据的 buffer size,默认:8k + max-bytes-in-message: 10MB # 消息解析最大 bytes 长度,默认:10M + auth: + enable: true # 是否开启 mqtt 认证 + username: admin # mqtt 认证用户名 + password: admin@123 # mqtt 认证密码 + debug: true # 如果开启 prometheus 指标收集建议关闭 + stat-enable: true # 开启指标收集,debug 和 prometheus 开启时需要打开,默认开启,关闭节省内存 + mqtt-listener: # mqtt 监听器 + enable: true # 是否开启,默认:false +# ip: "0.0.0.0" # 服务端 ip 默认为空,0.0.0.0,建议不要设置 + port: 1883 # 端口,默认:1883 + mqtt-ssl-listener: # mqtt ssl 监听器 + enable: false # 是否开启,默认:false + port: 8883 # 端口,默认:8883 + ssl: # ssl 配置,必须 + keystore-path: # 必须参数:ssl keystore 目录,支持 classpath:/ 路径。 + keystore-pass: # 必选参数:ssl keystore 密码 + truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 + truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 + client-auth: none # 是否需要客户端认证(双向认证),默认:NONE(不需要) + ws-listener: # websocket mqtt 监听器 + enable: true # 是否开启,默认:false + port: 8083 # websocket 端口,默认:8083 + wss-listener: # websocket ssl mqtt 监听器 + enable: false # 是否开启,默认:false + port: 8084 # 端口,默认:8084 + ssl: # ssl 配置,必须 + keystore-path: # 必须参数:ssl keystore 目录,支持 classpath:/ 路径。 + keystore-pass: # 必选参数:ssl keystore 密码 + truststore-path: # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。 + truststore-pass: # 可选参数:ssl 双向认证 truststore 密码 + client-auth: none # 是否需要客户端认证(双向认证),默认:NONE(不需要) + http-listener: + enable: true + port: 18083 + basic-auth: # 基础认证 + enable: true + username: mica + password: mica + mcp-server: # 大模型 mcp + enable: true + +springdoc: + swagger-ui: + urls: + - name: swagger + url: /v3/api-docs + +# actuator management +management: + info: + defaults: + enabled: true + metrics: + tags: + application: ${spring.application.name} + endpoint: + health: + show-details: ALWAYS + prometheus: + enabled: true + endpoints: + web: + exposure: + include: '*' + +logging: + level: + root: info + server: info # t-io 服务端默认日志 + org.tio: info # t-io 服务端默认日志 + org.dromara.mica.mqtt: info # mica-mqtt 日志 diff --git a/system/mqtt-xf-receiver/src/main/resources/banner.txt b/system/mqtt-xf-receiver/src/main/resources/banner.txt new file mode 100644 index 0000000..2eb7254 --- /dev/null +++ b/system/mqtt-xf-receiver/src/main/resources/banner.txt @@ -0,0 +1,12 @@ + +${AnsiColor.BRIGHT_BLUE}## ## #### ###### ### ${AnsiColor.RED} ## ## ####### ######## ######## +${AnsiColor.BRIGHT_BLUE}### ### ## ## ## ## ## ${AnsiColor.RED} ### ### ## ## ## ## +${AnsiColor.BRIGHT_BLUE}#### #### ## ## ## ## ${AnsiColor.RED} #### #### ## ## ## ## +${AnsiColor.BRIGHT_BLUE}## ### ## ## ## ## ##${AnsiColor.RED} ## ### ## ## ## ## ## +${AnsiColor.BRIGHT_BLUE}## ## ## ## #########${AnsiColor.RED} ## ## ## ## ## ## ## +${AnsiColor.BRIGHT_BLUE}## ## ## ## ## ## ##${AnsiColor.RED} ## ## ## ## ## ## +${AnsiColor.BRIGHT_BLUE}## ## #### ###### ## ##${AnsiColor.RED} ## ## ##### ## ## ## + + https://www.dreamlu.net + +${AnsiColor.BRIGHT_BLUE}:: ${spring.application.name} :: Running Spring Boot ${spring-boot.version} 🏃🏃🏃 ${AnsiColor.DEFAULT} diff --git a/system/pom.xml b/system/pom.xml new file mode 100644 index 0000000..3b1a4bb --- /dev/null +++ b/system/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + mica-mqtt + ${revision} + + system + ${project.artifactId} + pom + + + + 17 + + 4.0.0 + + 4.0.0 + + 2.13.2 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + com.google.code.gson + gson + ${gson.version} + + + com.google.errorprone + error_prone_annotations + + + + + + net.dreamlu + mica-bom + ${mica.version} + pom + import + + + + +