From 871ae8be0a987004f0abae1f9c750492881e8871 Mon Sep 17 00:00:00 2001 From: zc Date: Mon, 8 Dec 2025 10:40:43 +0800 Subject: [PATCH] first commit --- .codacy.yml | 7 + .editorconfig | 21 + .gitattributes | 22 + .gitignore | 65 ++ CHANGELOG.md | 625 +++++++++++++ LICENSE | 191 ++++ README.en.md | 129 +++ README.md | 180 ++++ SECURITY.md | 14 + deploy.sh | 43 + docs/graalvm.md | 2 + docs/http-api.md | 426 +++++++++ docs/http/http-client.env.json | 7 + docs/http/mica-mqtt-api.http | 132 +++ docs/img/dreamlu-weixin.jpg | Bin 0 -> 45580 bytes docs/img/mica-mqtt.jpg | Bin 0 -> 25522 bytes docs/update.md | 91 ++ example/README.md | 113 +++ .../pom.xml | 33 + .../client/solon/MqttClientApplication.java | 19 + .../solon/controller/ClientController.java | 26 + .../listener/MqttClientConnectedListener.java | 41 + .../MqttClientDisconnectListener.java | 45 + .../listener/MqttClientMessageListener.java | 25 + .../listener/MqttClientSubscribeListener.java | 68 ++ .../mica/mqtt/client/solon/pojo/User.java | 9 + .../client/solon/service/ClientService.java | 32 + .../src/main/resources/app.yml | 36 + .../README.md | 6 + .../pom.xml | 62 ++ .../mqtt/client/MqttClientApplication.java | 21 + .../config/OtherMqttClientConfiguration.java | 31 + .../client/controller/ClientController.java | 32 + .../listener/MqttClientConnectListener.java | 53 ++ .../listener/MqttClientMessageListener.java | 27 + .../listener/MqttClientSubscribeListener.java | 70 ++ .../OtherMqttClientSubscribeListener.java | 29 + .../dromara/mica/mqtt/client/pojo/User.java | 9 + .../mqtt/client/service/ClientService.java | 52 ++ .../mqtt/client/service/HelloInterfaceA.java | 31 + .../mqtt/client/service/HelloInterfaceB.java | 32 + .../src/main/resources/application.yml | 46 + .../src/main/resources/banner.txt | 12 + example/mica-mqtt-example/pom.xml | 183 ++++ .../mica/mqtt/aliyun/MqttClientTest.java | 65 ++ .../dromara/mica/mqtt/aliyun/MqttSign.java | 81 ++ .../mica/mqtt/benchmark/MqttBenchmark.java | 72 ++ .../mica/mqtt/benchmark/MqttPublishBench.java | 59 ++ .../mica/mqtt/benchmark/MqttServerBench.java | 38 + .../org/dromara/mica/mqtt/broker/DeviceA.java | 47 + .../org/dromara/mica/mqtt/broker/DeviceB.java | 46 + .../org/dromara/mica/mqtt/broker/DeviceC.java | 43 + .../org/dromara/mica/mqtt/broker/DeviceD.java | 36 + .../org/dromara/mica/mqtt/broker/Server.java | 42 + .../mica/mqtt/client/Mqtt5ClientTest.java | 79 ++ .../mica/mqtt/client/MqttClientConnTest.java | 40 + .../client/MqttClientConnectListener.java | 48 + .../mqtt/client/MqttClientGlobalTest.java | 50 + .../mica/mqtt/client/MqttClientSyncTest.java | 55 ++ .../mica/mqtt/client/MqttClientTest.java | 129 +++ .../mica/mqtt/huawei/MqttClientTest.java | 70 ++ .../dromara/mica/mqtt/huawei/MqttSign.java | 89 ++ .../mqtt/nginx/MqttServerProxyProtocol.java | 40 + .../mica/mqtt/proxy/MqttClientProxy.java | 58 ++ .../mica/mqtt/proxy/MqttServerProxy.java | 67 ++ .../server/MqttConnectStatusListener.java | 42 + .../mqtt/server/MqttMessageInterceptor.java | 58 ++ .../mica/mqtt/server/MqttServerTest.java | 74 ++ .../mica/mqtt/ssl/SslMqttClientTest.java | 54 ++ .../mica/mqtt/ssl/SslMqttServerTest.java | 54 ++ .../src/main/resources/ssl/README.md | 1 + .../src/main/resources/ssl/ca-cert.pem | 16 + .../src/main/resources/ssl/client-cert.pem | 16 + .../src/main/resources/ssl/client-key.pem | 15 + .../src/main/resources/ssl/dreamlu.net.jks | Bin 0 -> 1414 bytes .../src/main/resources/tinylog.properties | 8 + .../java/net/dreamlu/iot/package-info.java | 1 + .../src/test/resources/test.yml | 0 .../pom.xml | 49 + .../server/solon/MqttServerApplication.java | 19 + .../solon/controller/ServerController.java | 22 + .../listener/MqttConnectOfflineListener.java | 37 + .../listener/MqttConnectOnlineListener.java | 37 + .../listener/MqttServerMessageListener1.java | 25 + .../listener/MqttServerMessageListener2.java | 48 + .../mica/mqtt/server/solon/pojo/User.java | 19 + .../server/solon/service/ServerService.java | 24 + .../mqtt/server/solon/task/PublishTask.java | 25 + .../src/main/resources/app.yml | 54 ++ .../README.md | 14 + .../pom.xml | 92 ++ .../mqtt/server/MqttServerApplication.java | 21 + .../mqtt/server/auth/MqttAuthHandler.java | 21 + .../mqtt/server/auth/MqttHttpAuthFilter.java | 36 + .../server/auth/MqttSubscribeValidator.java | 22 + .../mqtt/server/auth/MqttUniqueIdService.java | 21 + .../server/controller/CarInfoController.java | 26 + .../server/controller/ServerController.java | 25 + .../mica/mqtt/server/entity/CarInfo.java | 12 + .../listener/MqttConnectStatusListener1.java | 43 + .../listener/MqttConnectStatusListener2.java | 42 + .../listener/MqttServerMessageListener1.java | 37 + .../listener/MqttServerMessageListener2.java | 55 ++ .../mqtt/server/mapper/CarInfoMapper.java | 9 + .../dromara/mica/mqtt/server/pojo/User.java | 19 + .../mqtt/server/service/ICarInfoService.java | 7 + .../mqtt/server/service/ServerService.java | 24 + .../service/impl/CarInfoServiceImpl.java | 11 + .../mica/mqtt/server/task/PublishAllTask.java | 25 + .../src/main/resources/application.yml | 104 +++ .../src/main/resources/banner.txt | 12 + example/pom.xml | 71 ++ mica-mqtt-client/README.md | 157 ++++ mica-mqtt-client/pom.xml | 36 + .../client/DefaultMqttClientProcessor.java | 419 +++++++++ .../core/client/DefaultMqttClientSession.java | 206 +++++ .../mica/mqtt/core/client/IMqttClient.java | 48 + .../client/IMqttClientConnectListener.java | 46 + .../IMqttClientGlobalMessageListener.java | 40 + .../client/IMqttClientMessageListener.java | 65 ++ .../core/client/IMqttClientProcessor.java | 90 ++ .../mqtt/core/client/IMqttClientSession.java | 208 +++++ .../mica/mqtt/core/client/MqttClient.java | 640 +++++++++++++ .../core/client/MqttClientAioHandler.java | 98 ++ .../core/client/MqttClientAioListener.java | 132 +++ .../MqttClientConnectTestProcessor.java | 81 ++ .../mqtt/core/client/MqttClientCreator.java | 802 ++++++++++++++++ .../core/client/MqttClientCustomizer.java | 34 + .../mica/mqtt/core/client/MqttClientId.java | 21 + .../core/client/MqttClientSubscription.java | 93 ++ .../core/client/MqttInvocationHandler.java | 160 ++++ .../core/client/MqttPendingSubscription.java | 62 ++ .../client/MqttPendingUnSubscription.java | 61 ++ .../core/client/MqttSSLEngineCustomizer.java | 66 ++ .../mqtt/core/client/MqttWillMessage.java | 169 ++++ .../src/main/moditect/module-info.java | 4 + mica-mqtt-codec/pom.xml | 37 + .../mica/mqtt/codec/MqttCodecUtil.java | 155 ++++ .../dromara/mica/mqtt/codec/MqttConstant.java | 52 ++ .../dromara/mica/mqtt/codec/MqttDecoder.java | 727 +++++++++++++++ .../dromara/mica/mqtt/codec/MqttEncoder.java | 618 +++++++++++++ .../mica/mqtt/codec/MqttMessageFactory.java | 86 ++ .../mica/mqtt/codec/MqttMessageType.java | 119 +++ .../org/dromara/mica/mqtt/codec/MqttQoS.java | 72 ++ .../dromara/mica/mqtt/codec/MqttVersion.java | 83 ++ .../mqtt/codec/codes/MqttAuthReasonCode.java | 47 + .../codec/codes/MqttConnectReasonCode.java | 96 ++ .../codec/codes/MqttDisconnectReasonCode.java | 79 ++ .../codec/codes/MqttPubAckReasonCode.java | 60 ++ .../codec/codes/MqttPubCompReasonCode.java | 52 ++ .../codec/codes/MqttPubRecReasonCode.java | 65 ++ .../codec/codes/MqttPubRelReasonCode.java | 52 ++ .../mica/mqtt/codec/codes/MqttReasonCode.java | 42 + .../codec/codes/MqttSubAckReasonCode.java | 77 ++ .../codec/codes/MqttUnSubAckReasonCode.java | 48 + .../mqtt/codec/codes/ReasonCodeUtils.java | 47 + .../codec/exception/DecoderException.java | 45 + .../codec/exception/EncoderException.java | 61 ++ .../MqttIdentifierRejectedException.java | 60 ++ ...tUnacceptableProtocolVersionException.java | 62 ++ .../codec/message/MqttConnAckMessage.java | 47 + .../codec/message/MqttConnectMessage.java | 56 ++ .../mica/mqtt/codec/message/MqttMessage.java | 77 ++ .../mqtt/codec/message/MqttPubAckMessage.java | 47 + .../codec/message/MqttPublishMessage.java | 58 ++ .../mqtt/codec/message/MqttSubAckMessage.java | 68 ++ .../codec/message/MqttSubscribeMessage.java | 69 ++ .../codec/message/MqttUnSubAckMessage.java | 81 ++ .../codec/message/MqttUnSubscribeMessage.java | 69 ++ .../message/builder/MqttAuthBuilder.java | 70 ++ .../message/builder/MqttConnAckBuilder.java | 71 ++ .../message/builder/MqttConnectBuilder.java | 165 ++++ .../builder/MqttDisconnectBuilder.java | 65 ++ .../message/builder/MqttPubAckBuilder.java | 75 ++ .../message/builder/MqttPublishBuilder.java | 109 +++ .../message/builder/MqttSubAckBuilder.java | 106 +++ .../message/builder/MqttSubscribeBuilder.java | 88 ++ .../builder/MqttSubscriptionOption.java | 144 +++ .../builder/MqttTopicSubscription.java | 77 ++ .../message/builder/MqttUnSubAckBuilder.java | 97 ++ .../builder/MqttUnSubscribeBuilder.java | 80 ++ .../header/MqttConnAckVariableHeader.java | 65 ++ .../header/MqttConnectVariableHeader.java | 138 +++ .../codec/message/header/MqttFixedHeader.java | 108 +++ ...tMessageIdAndPropertiesVariableHeader.java | 53 ++ .../header/MqttMessageIdVariableHeader.java | 59 ++ .../MqttPubReplyMessageVariableHeader.java | 56 ++ .../header/MqttPublishVariableHeader.java | 61 ++ ...ReasonCodeAndPropertiesVariableHeader.java | 53 ++ .../message/payload/MqttConnectPayload.java | 105 +++ .../message/payload/MqttSubAckPayload.java | 78 ++ .../message/payload/MqttSubscribePayload.java | 53 ++ .../message/payload/MqttUnsubAckPayload.java | 75 ++ .../payload/MqttUnsubscribePayload.java | 52 ++ .../properties/MqttAuthProperties.java | 97 ++ .../properties/MqttConnAckProperties.java | 239 +++++ .../properties/MqttConnectProperties.java | 151 +++ .../properties/MqttDisconnectProperties.java | 97 ++ .../properties/MqttPubAckProperties.java | 77 ++ .../properties/MqttPubCompProperties.java | 77 ++ .../properties/MqttPubRecProperties.java | 77 ++ .../properties/MqttPubRelProperties.java | 77 ++ .../properties/MqttPublishProperties.java | 140 +++ .../properties/MqttSubAckProperties.java | 78 ++ .../properties/MqttSubscribeProperties.java | 77 ++ .../properties/MqttUnSubAckProperties.java | 77 ++ .../properties/MqttUnSubscribeProperties.java | 65 ++ .../properties/MqttWillPublishProperties.java | 130 +++ .../mqtt/codec/properties/BinaryProperty.java | 33 + .../codec/properties/BooleanProperty.java | 33 + .../codec/properties/IntegerProperty.java | 33 + .../mqtt/codec/properties/MqttProperties.java | 193 ++++ .../mqtt/codec/properties/MqttProperty.java | 67 ++ .../codec/properties/MqttPropertyType.java | 175 ++++ .../mqtt/codec/properties/StringPair.java | 45 + .../mqtt/codec/properties/StringProperty.java | 34 + .../mqtt/codec/properties/UserProperties.java | 70 ++ .../mqtt/codec/properties/UserProperty.java | 29 + .../src/main/moditect/module-info.java | 12 + .../mqtt/codec/test/MqttCodecUtilTest.java | 37 + mica-mqtt-common/README.md | 2 + mica-mqtt-common/pom.xml | 51 ++ .../core/annotation/MqttClientPublish.java | 49 + .../core/annotation/MqttClientSubscribe.java | 68 ++ .../mqtt/core/annotation/MqttPayload.java | 31 + .../mica/mqtt/core/annotation/MqttRetain.java | 32 + .../core/annotation/MqttServerFunction.java | 48 + .../mqtt/core/common/MqttPendingPublish.java | 93 ++ .../core/common/MqttPendingQos2Publish.java | 63 ++ .../mica/mqtt/core/common/RetryProcessor.java | 51 ++ .../mica/mqtt/core/common/TopicFilter.java | 65 ++ .../mqtt/core/common/TopicFilterType.java | 153 ++++ .../mica/mqtt/core/common/TopicTemplate.java | 43 + .../core/deserialize/MqttDeserializer.java | 39 + .../deserialize/MqttJsonDeserializer.java | 36 + .../function/ObjectParamValueFunction.java | 43 + .../core/function/ParamValueFunction.java | 41 + .../core/function/ParamValueFunctions.java | 66 ++ .../function/TopicVarsParamValueFunction.java | 68 ++ .../core/serializer/MqttJsonSerializer.java | 33 + .../mqtt/core/serializer/MqttSerializer.java | 34 + .../mica/mqtt/core/util/MethodParamUtil.java | 93 ++ .../mica/mqtt/core/util/TopicUtil.java | 370 ++++++++ .../mqtt/core/util/timer/AckTimerTask.java | 73 ++ .../src/main/moditect/module-info.java | 11 + .../mqtt/core/common/TopicFilterTypeTest.java | 42 + .../mica/mqtt/core/timer/SystemTimerTest.java | 25 + .../mica/mqtt/core/udp/UdpCluster.java | 115 +++ .../mica/mqtt/core/udp/UdpClusterConfig.java | 32 + .../mica/mqtt/core/udp/UdpClusterTest1.java | 29 + .../mica/mqtt/core/udp/UdpClusterTest2.java | 31 + .../mica/mqtt/core/udp/UdpTestHandler.java | 35 + .../dromara/mica/mqtt/core/util/TestBean.java | 40 + .../mica/mqtt/core/util/TopicUtilTest.java | 159 ++++ mica-mqtt-server/README.md | 92 ++ mica-mqtt-server/pom.xml | 57 ++ .../mica/mqtt/core/server/MqttConst.java | 31 + .../core/server/MqttMessageInterceptors.java | 116 +++ .../mica/mqtt/core/server/MqttServer.java | 564 ++++++++++++ .../core/server/MqttServerAioHandler.java | 169 ++++ .../core/server/MqttServerAioListener.java | 159 ++++ .../mqtt/core/server/MqttServerCreator.java | 590 ++++++++++++ .../core/server/MqttServerCustomizer.java | 34 + .../mqtt/core/server/MqttServerProcessor.java | 111 +++ .../server/auth/IMqttServerAuthHandler.java | 63 ++ .../auth/IMqttServerPublishPermission.java | 63 ++ .../auth/IMqttServerSubscribeValidator.java | 61 ++ .../auth/IMqttServerUniqueIdService.java | 39 + .../broker/DefaultMqttBrokerDispatcher.java | 34 + .../cluster/MqttClusterMessageListener.java | 129 +++ .../AbstractMqttMessageDispatcher.java | 122 +++ .../dispatcher/IMqttMessageDispatcher.java | 45 + .../mqtt/core/server/enums/MessageType.java | 88 ++ .../event/IMqttConnectStatusListener.java | 47 + .../server/event/IMqttMessageListener.java | 42 + .../server/event/IMqttSessionListener.java | 48 + .../func/IMqttFunctionMessageListener.java | 42 + .../core/server/func/MqttFunctionManager.java | 202 ++++ .../func/MqttFunctionMessageListener.java | 53 ++ .../core/server/http/api/MqttHttpApi.java | 473 ++++++++++ .../server/http/api/auth/BasicAuthFilter.java | 74 ++ .../core/server/http/api/code/ResultCode.java | 72 ++ .../core/server/http/api/form/BaseForm.java | 53 ++ .../server/http/api/form/PublishForm.java | 74 ++ .../server/http/api/form/SubscribeForm.java | 38 + .../core/server/http/api/result/Result.java | 134 +++ .../core/server/http/handler/HttpFilter.java | 46 + .../http/handler/MqttHttpRequestHandler.java | 84 ++ .../server/http/handler/MqttHttpRoutes.java | 103 +++ .../core/server/http/handler/RouteInfo.java | 69 ++ .../mqtt/core/server/http/mcp/MqttMcp.java | 291 ++++++ .../http/websocket/MqttWsMsgHandler.java | 205 +++++ .../http/websocket/MqttWsServerListener.java | 71 ++ .../interceptor/IMqttMessageInterceptor.java | 87 ++ .../listener/IMqttProtocolListener.java | 72 ++ .../server/listener/MqttHttpApiListener.java | 214 +++++ .../server/listener/MqttProtocolListener.java | 257 ++++++ .../listener/MqttProtocolListeners.java | 82 ++ .../mqtt/core/server/model/ClientInfo.java | 317 +++++++ .../mica/mqtt/core/server/model/Message.java | 251 +++++ .../mqtt/core/server/model/Subscribe.java | 96 ++ .../core/server/protocol/MqttProtocol.java | 58 ++ .../serializer/DefaultMessageSerializer.java | 861 ++++++++++++++++++ .../server/serializer/IMessageSerializer.java | 44 + .../serializer/JsonMessageSerializer.java | 39 + .../server/session/IMqttSessionManager.java | 183 ++++ .../session/InMemoryMqttSessionManager.java | 168 ++++ .../core/server/session/SharedStrategy.java | 43 + .../core/server/session/TrieTopicManager.java | 513 +++++++++++ .../core/server/store/IMqttMessageStore.java | 88 ++ .../store/InMemoryMqttMessageStore.java | 113 +++ .../DefaultMqttConnectStatusListener.java | 41 + .../support/DefaultMqttServerAuthHandler.java | 43 + .../support/DefaultMqttServerProcessor.java | 545 +++++++++++ .../DefaultMqttServerUniqueIdServiceImpl.java | 34 + .../src/main/moditect/module-info.java | 22 + .../server/test/MqttSessionManagerTest.java | 90 ++ .../test/TrieTopicManagerDeepAnalysis.java | 288 ++++++ .../TrieTopicManagerMemoryAnalysisTest.java | 215 +++++ .../server/test/TrieTopicManagerTest.java | 178 ++++ .../server/test/cluster/ClusterTest1.java | 36 + .../server/test/cluster/ClusterTest2.java | 34 + .../server/test/cluster/ClusterTest3.java | 34 + .../server/test/cluster/ClusterTest4.java | 35 + .../src/test/resources/tinylog.properties | 7 + pom.xml | 404 ++++++++ starter/README.md | 16 + .../mica-mqtt-client-jfinal-plugin/README.md | 81 ++ .../mica-mqtt-client-jfinal-plugin/pom.xml | 32 + .../mqtt/jfinal/client/MqttClientKit.java | 354 +++++++ .../mqtt/jfinal/client/MqttClientPlugin.java | 72 ++ .../src/main/moditect/module-info.java | 5 + .../jfinal/client/MqttClientPluginTest.java | 19 + .../mica-mqtt-client-solon-plugin/README.md | 197 ++++ starter/mica-mqtt-client-solon-plugin/pom.xml | 42 + .../solon/MqttClientSubscribeListener.java | 78 ++ .../mqtt/client/solon/MqttClientTemplate.java | 458 ++++++++++ .../mqtt/client/solon/config/DataSize.java | 195 ++++ .../solon/config/MqttClientConfiguration.java | 106 +++ .../solon/config/MqttClientProperties.java | 211 +++++ .../solon/event/MqttConnectedEvent.java | 40 + .../solon/event/MqttDisconnectEvent.java | 44 + .../SolonEventMqttClientConnectListener.java | 55 ++ .../integration/MqttClientPluginImpl.java | 173 ++++ .../src/main/moditect/module-info.java | 9 + .../solon/solon-configuration-metadata.json | 199 ++++ .../solon/solon.mica.client.properties | 2 + .../mqtt/client/solon/test/ClientTest.java | 32 + .../listener/MqttClientConnectListener.java | 43 + .../MqttClientDisConnectListener.java | 47 + .../listener/MqttClientMessageListener.java | 26 + .../listener/MqttClientSubscribeListener.java | 38 + .../src/test/resources/app.yml | 27 + .../README.md | 269 ++++++ .../pom.xml | 38 + .../spring/client/MqttClientFactoryBean.java | 56 ++ .../spring/client/MqttClientRegistrar.java | 98 ++ .../client/MqttClientSubscribeDetector.java | 153 ++++ .../client/MqttClientSubscribeLazyFilter.java | 54 ++ .../client/MqttClientSubscribeListener.java | 74 ++ .../spring/client/MqttClientTemplate.java | 449 +++++++++ .../client/annotation/EnableMqttClients.java | 38 + .../spring/client/annotation/MqttClient.java | 39 + .../config/MqttClientConfiguration.java | 150 +++ .../client/config/MqttClientProperties.java | 217 +++++ .../client/event/MqttConnectedEvent.java | 45 + .../client/event/MqttDisconnectEvent.java | 49 + .../SpringEventMqttClientConnectListener.java | 58 ++ .../src/main/moditect/module-info.java | 12 + .../mica-mqtt-server-jfinal-plugin/README.md | 43 + .../mica-mqtt-server-jfinal-plugin/pom.xml | 32 + .../mqtt/jfinal/server/MqttServerKit.java | 159 ++++ .../mqtt/jfinal/server/MqttServerPlugin.java | 64 ++ .../src/main/moditect/module-info.java | 5 + .../jfinal/server/MqttServerPluginTest.java | 23 + .../mica-mqtt-server-solon-plugin/README.md | 199 ++++ starter/mica-mqtt-server-solon-plugin/pom.xml | 54 ++ .../mqtt/server/solon/MqttServerTemplate.java | 267 ++++++ .../mqtt/server/solon/config/DataSize.java | 195 ++++ .../solon/config/MqttServerConfiguration.java | 119 +++ .../solon/config/MqttServerMetrics.java | 107 +++ .../MqttServerMetricsConfiguration.java | 48 + .../solon/config/MqttServerProperties.java | 240 +++++ .../solon/event/MqttClientOfflineEvent.java | 48 + .../solon/event/MqttClientOnlineEvent.java | 56 ++ .../SolonEventMqttConnectStatusListener.java | 63 ++ .../MqttServerFunctionListener.java | 78 ++ .../integration/MqttServerPluginImpl.java | 240 +++++ .../src/main/moditect/module-info.java | 9 + .../solon/solon-configuration-metadata.json | 269 ++++++ .../solon/solon.mica.server.properties | 2 + .../server/solon/test/task/ServerTest.java | 20 + .../solon/test/task/task/PublishAllTask.java | 24 + .../src/test/resources/app.yml | 20 + .../README.md | 202 ++++ .../pom.xml | 50 + .../server/MqttServerFunctionDetector.java | 132 +++ .../server/MqttServerFunctionLazyFilter.java | 54 ++ .../server/MqttServerFunctionListener.java | 73 ++ .../spring/server/MqttServerTemplate.java | 267 ++++++ .../config/MqttServerConfiguration.java | 216 +++++ .../server/config/MqttServerLauncher.java | 72 ++ .../server/config/MqttServerMetrics.java | 132 +++ .../MqttServerMetricsConfiguration.java | 47 + .../server/config/MqttServerProperties.java | 251 +++++ .../server/event/MqttClientOfflineEvent.java | 48 + .../server/event/MqttClientOnlineEvent.java | 56 ++ .../SpringEventMqttConnectStatusListener.java | 66 ++ .../src/main/moditect/module-info.java | 11 + starter/pom.xml | 33 + 410 files changed, 38212 insertions(+) create mode 100644 .codacy.yml create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.en.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 deploy.sh create mode 100644 docs/graalvm.md create mode 100644 docs/http-api.md create mode 100644 docs/http/http-client.env.json create mode 100644 docs/http/mica-mqtt-api.http create mode 100644 docs/img/dreamlu-weixin.jpg create mode 100644 docs/img/mica-mqtt.jpg create mode 100644 docs/update.md create mode 100644 example/README.md create mode 100644 example/mica-mqtt-client-solon-plugin-example/pom.xml create mode 100644 example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientApplication.java create mode 100644 example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/controller/ClientController.java create mode 100644 example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientConnectedListener.java create mode 100644 example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientDisconnectListener.java create mode 100644 example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientMessageListener.java create mode 100644 example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientSubscribeListener.java create mode 100644 example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/pojo/User.java create mode 100644 example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/service/ClientService.java create mode 100644 example/mica-mqtt-client-solon-plugin-example/src/main/resources/app.yml create mode 100644 example/mica-mqtt-client-spring-boot-example/README.md create mode 100644 example/mica-mqtt-client-spring-boot-example/pom.xml create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientApplication.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/config/OtherMqttClientConfiguration.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/controller/ClientController.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientConnectListener.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientMessageListener.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientSubscribeListener.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/OtherMqttClientSubscribeListener.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/pojo/User.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/ClientService.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/HelloInterfaceA.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/HelloInterfaceB.java create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/resources/application.yml create mode 100644 example/mica-mqtt-client-spring-boot-example/src/main/resources/banner.txt create mode 100644 example/mica-mqtt-example/pom.xml create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/aliyun/MqttClientTest.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/aliyun/MqttSign.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttBenchmark.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttPublishBench.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttServerBench.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceA.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceB.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceC.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceD.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/Server.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/Mqtt5ClientTest.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientConnTest.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientConnectListener.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientGlobalTest.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientSyncTest.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientTest.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/huawei/MqttClientTest.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/huawei/MqttSign.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/nginx/MqttServerProxyProtocol.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/proxy/MqttClientProxy.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/proxy/MqttServerProxy.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttConnectStatusListener.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttMessageInterceptor.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttServerTest.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/ssl/SslMqttClientTest.java create mode 100644 example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/ssl/SslMqttServerTest.java create mode 100644 example/mica-mqtt-example/src/main/resources/ssl/README.md create mode 100644 example/mica-mqtt-example/src/main/resources/ssl/ca-cert.pem create mode 100644 example/mica-mqtt-example/src/main/resources/ssl/client-cert.pem create mode 100644 example/mica-mqtt-example/src/main/resources/ssl/client-key.pem create mode 100644 example/mica-mqtt-example/src/main/resources/ssl/dreamlu.net.jks create mode 100644 example/mica-mqtt-example/src/main/resources/tinylog.properties create mode 100644 example/mica-mqtt-example/src/test/java/net/dreamlu/iot/package-info.java create mode 100644 example/mica-mqtt-example/src/test/resources/test.yml create mode 100644 example/mica-mqtt-server-solon-plugin-example/pom.xml create mode 100644 example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/MqttServerApplication.java create mode 100644 example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/controller/ServerController.java create mode 100644 example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttConnectOfflineListener.java create mode 100644 example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttConnectOnlineListener.java create mode 100644 example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttServerMessageListener1.java create mode 100644 example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttServerMessageListener2.java create mode 100644 example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/pojo/User.java create mode 100644 example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/service/ServerService.java create mode 100644 example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/task/PublishTask.java create mode 100644 example/mica-mqtt-server-solon-plugin-example/src/main/resources/app.yml create mode 100644 example/mica-mqtt-server-spring-boot-example/README.md create mode 100644 example/mica-mqtt-server-spring-boot-example/pom.xml create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/MqttServerApplication.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttAuthHandler.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttHttpAuthFilter.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttSubscribeValidator.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttUniqueIdService.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/controller/CarInfoController.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/controller/ServerController.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/entity/CarInfo.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener1.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener2.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttServerMessageListener1.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttServerMessageListener2.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/mapper/CarInfoMapper.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/pojo/User.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/ICarInfoService.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/ServerService.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/impl/CarInfoServiceImpl.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/task/PublishAllTask.java create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/resources/application.yml create mode 100644 example/mica-mqtt-server-spring-boot-example/src/main/resources/banner.txt create mode 100644 example/pom.xml create mode 100644 mica-mqtt-client/README.md create mode 100644 mica-mqtt-client/pom.xml create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/DefaultMqttClientProcessor.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/DefaultMqttClientSession.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClient.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientConnectListener.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientGlobalMessageListener.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientMessageListener.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientProcessor.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/IMqttClientSession.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClient.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientAioHandler.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientAioListener.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientConnectTestProcessor.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientCreator.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientCustomizer.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientId.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttClientSubscription.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttInvocationHandler.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttPendingSubscription.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttPendingUnSubscription.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttSSLEngineCustomizer.java create mode 100644 mica-mqtt-client/src/main/java/org/dromara/mica/mqtt/core/client/MqttWillMessage.java create mode 100644 mica-mqtt-client/src/main/moditect/module-info.java create mode 100644 mica-mqtt-codec/pom.xml create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttCodecUtil.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttConstant.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttDecoder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttEncoder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttMessageFactory.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttMessageType.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttQoS.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/MqttVersion.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttAuthReasonCode.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttConnectReasonCode.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttDisconnectReasonCode.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubAckReasonCode.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubCompReasonCode.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubRecReasonCode.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttPubRelReasonCode.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttReasonCode.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttSubAckReasonCode.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/MqttUnSubAckReasonCode.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/codes/ReasonCodeUtils.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/DecoderException.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/EncoderException.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/MqttIdentifierRejectedException.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/exception/MqttUnacceptableProtocolVersionException.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttConnAckMessage.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttConnectMessage.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttMessage.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttPubAckMessage.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttPublishMessage.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttSubAckMessage.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttSubscribeMessage.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttUnSubAckMessage.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/MqttUnSubscribeMessage.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttAuthBuilder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttConnAckBuilder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttConnectBuilder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttDisconnectBuilder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttPubAckBuilder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttPublishBuilder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubAckBuilder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubscribeBuilder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttSubscriptionOption.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttTopicSubscription.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttUnSubAckBuilder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/builder/MqttUnSubscribeBuilder.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttConnAckVariableHeader.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttConnectVariableHeader.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttFixedHeader.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttMessageIdAndPropertiesVariableHeader.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttMessageIdVariableHeader.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttPubReplyMessageVariableHeader.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttPublishVariableHeader.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/header/MqttReasonCodeAndPropertiesVariableHeader.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttConnectPayload.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttSubAckPayload.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttSubscribePayload.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttUnsubAckPayload.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/payload/MqttUnsubscribePayload.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttAuthProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttConnAckProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttConnectProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttDisconnectProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubAckProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubCompProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubRecProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPubRelProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttPublishProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttSubAckProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttSubscribeProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttUnSubAckProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttUnSubscribeProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/message/properties/MqttWillPublishProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/BinaryProperty.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/BooleanProperty.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/IntegerProperty.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttProperty.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/MqttPropertyType.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/StringPair.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/StringProperty.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/UserProperties.java create mode 100644 mica-mqtt-codec/src/main/java/org/dromara/mica/mqtt/codec/properties/UserProperty.java create mode 100644 mica-mqtt-codec/src/main/moditect/module-info.java create mode 100644 mica-mqtt-codec/src/test/java/org/dromara/mica/mqtt/codec/test/MqttCodecUtilTest.java create mode 100644 mica-mqtt-common/README.md create mode 100644 mica-mqtt-common/pom.xml create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttClientPublish.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttClientSubscribe.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttPayload.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttRetain.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/annotation/MqttServerFunction.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/MqttPendingPublish.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/MqttPendingQos2Publish.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/RetryProcessor.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicFilter.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicFilterType.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/common/TopicTemplate.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/deserialize/MqttDeserializer.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/deserialize/MqttJsonDeserializer.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ObjectParamValueFunction.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ParamValueFunction.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/ParamValueFunctions.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/function/TopicVarsParamValueFunction.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/serializer/MqttJsonSerializer.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/serializer/MqttSerializer.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/MethodParamUtil.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/TopicUtil.java create mode 100644 mica-mqtt-common/src/main/java/org/dromara/mica/mqtt/core/util/timer/AckTimerTask.java create mode 100644 mica-mqtt-common/src/main/moditect/module-info.java create mode 100644 mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/common/TopicFilterTypeTest.java create mode 100644 mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/timer/SystemTimerTest.java create mode 100644 mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpCluster.java create mode 100644 mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterConfig.java create mode 100644 mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterTest1.java create mode 100644 mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpClusterTest2.java create mode 100644 mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/udp/UdpTestHandler.java create mode 100644 mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/util/TestBean.java create mode 100644 mica-mqtt-common/src/test/java/org/dromara/mica/mqtt/core/util/TopicUtilTest.java create mode 100644 mica-mqtt-server/README.md create mode 100644 mica-mqtt-server/pom.xml create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttConst.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttMessageInterceptors.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServer.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerAioHandler.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerAioListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerCreator.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerCustomizer.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/MqttServerProcessor.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerAuthHandler.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerPublishPermission.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerSubscribeValidator.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/auth/IMqttServerUniqueIdService.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/broker/DefaultMqttBrokerDispatcher.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/cluster/MqttClusterMessageListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/dispatcher/AbstractMqttMessageDispatcher.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/dispatcher/IMqttMessageDispatcher.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/enums/MessageType.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttConnectStatusListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttMessageListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/event/IMqttSessionListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/IMqttFunctionMessageListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/MqttFunctionManager.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/func/MqttFunctionMessageListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/MqttHttpApi.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/auth/BasicAuthFilter.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/code/ResultCode.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/BaseForm.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/PublishForm.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/form/SubscribeForm.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/api/result/Result.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/HttpFilter.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/MqttHttpRequestHandler.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/MqttHttpRoutes.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/handler/RouteInfo.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/mcp/MqttMcp.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/websocket/MqttWsMsgHandler.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/http/websocket/MqttWsServerListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/interceptor/IMqttMessageInterceptor.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/IMqttProtocolListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttHttpApiListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttProtocolListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/listener/MqttProtocolListeners.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/ClientInfo.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/Message.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/model/Subscribe.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/protocol/MqttProtocol.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/DefaultMessageSerializer.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/IMessageSerializer.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/serializer/JsonMessageSerializer.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/IMqttSessionManager.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/InMemoryMqttSessionManager.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/SharedStrategy.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/session/TrieTopicManager.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/store/IMqttMessageStore.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/store/InMemoryMqttMessageStore.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttConnectStatusListener.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerAuthHandler.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerProcessor.java create mode 100644 mica-mqtt-server/src/main/java/org/dromara/mica/mqtt/core/server/support/DefaultMqttServerUniqueIdServiceImpl.java create mode 100644 mica-mqtt-server/src/main/moditect/module-info.java create mode 100644 mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/MqttSessionManagerTest.java create mode 100644 mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerDeepAnalysis.java create mode 100644 mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerMemoryAnalysisTest.java create mode 100644 mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/TrieTopicManagerTest.java create mode 100644 mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest1.java create mode 100644 mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest2.java create mode 100644 mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest3.java create mode 100644 mica-mqtt-server/src/test/java/org/dromara/mica/mqtt/core/server/test/cluster/ClusterTest4.java create mode 100644 mica-mqtt-server/src/test/resources/tinylog.properties create mode 100644 pom.xml create mode 100644 starter/README.md create mode 100644 starter/mica-mqtt-client-jfinal-plugin/README.md create mode 100644 starter/mica-mqtt-client-jfinal-plugin/pom.xml create mode 100644 starter/mica-mqtt-client-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/client/MqttClientKit.java create mode 100644 starter/mica-mqtt-client-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/client/MqttClientPlugin.java create mode 100644 starter/mica-mqtt-client-jfinal-plugin/src/main/moditect/module-info.java create mode 100644 starter/mica-mqtt-client-jfinal-plugin/src/test/java/org/dromara/mica/mqtt/jfinal/client/MqttClientPluginTest.java create mode 100644 starter/mica-mqtt-client-solon-plugin/README.md create mode 100644 starter/mica-mqtt-client-solon-plugin/pom.xml create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientSubscribeListener.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientTemplate.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/DataSize.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/MqttClientConfiguration.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/config/MqttClientProperties.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/MqttConnectedEvent.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/MqttDisconnectEvent.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/event/SolonEventMqttClientConnectListener.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/java/org/dromara/mica/mqtt/client/solon/integration/MqttClientPluginImpl.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/moditect/module-info.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/resources/META-INF/solon/solon-configuration-metadata.json create mode 100644 starter/mica-mqtt-client-solon-plugin/src/main/resources/META-INF/solon/solon.mica.client.properties create mode 100644 starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/ClientTest.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientConnectListener.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientDisConnectListener.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientMessageListener.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/test/java/org/dromara/mica/mqtt/client/solon/test/listener/MqttClientSubscribeListener.java create mode 100644 starter/mica-mqtt-client-solon-plugin/src/test/resources/app.yml create mode 100644 starter/mica-mqtt-client-spring-boot-starter/README.md create mode 100644 starter/mica-mqtt-client-spring-boot-starter/pom.xml create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientFactoryBean.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientRegistrar.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeDetector.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeLazyFilter.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientSubscribeListener.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/MqttClientTemplate.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/annotation/EnableMqttClients.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/annotation/MqttClient.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/config/MqttClientConfiguration.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/config/MqttClientProperties.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/MqttConnectedEvent.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/MqttDisconnectEvent.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/client/event/SpringEventMqttClientConnectListener.java create mode 100644 starter/mica-mqtt-client-spring-boot-starter/src/main/moditect/module-info.java create mode 100644 starter/mica-mqtt-server-jfinal-plugin/README.md create mode 100644 starter/mica-mqtt-server-jfinal-plugin/pom.xml create mode 100644 starter/mica-mqtt-server-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/server/MqttServerKit.java create mode 100644 starter/mica-mqtt-server-jfinal-plugin/src/main/java/org/dromara/mica/mqtt/jfinal/server/MqttServerPlugin.java create mode 100644 starter/mica-mqtt-server-jfinal-plugin/src/main/moditect/module-info.java create mode 100644 starter/mica-mqtt-server-jfinal-plugin/src/test/java/org/dromara/mica/mqtt/jfinal/server/MqttServerPluginTest.java create mode 100644 starter/mica-mqtt-server-solon-plugin/README.md create mode 100644 starter/mica-mqtt-server-solon-plugin/pom.xml create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/MqttServerTemplate.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/DataSize.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerConfiguration.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerMetrics.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerMetricsConfiguration.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/config/MqttServerProperties.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/MqttClientOfflineEvent.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/MqttClientOnlineEvent.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/event/SolonEventMqttConnectStatusListener.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/integration/MqttServerFunctionListener.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/java/org/dromara/mica/mqtt/server/solon/integration/MqttServerPluginImpl.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/moditect/module-info.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/resources/META-INF/solon/solon-configuration-metadata.json create mode 100644 starter/mica-mqtt-server-solon-plugin/src/main/resources/META-INF/solon/solon.mica.server.properties create mode 100644 starter/mica-mqtt-server-solon-plugin/src/test/java/org/dromara/mica/mqtt/server/solon/test/task/ServerTest.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/test/java/org/dromara/mica/mqtt/server/solon/test/task/task/PublishAllTask.java create mode 100644 starter/mica-mqtt-server-solon-plugin/src/test/resources/app.yml create mode 100644 starter/mica-mqtt-server-spring-boot-starter/README.md create mode 100644 starter/mica-mqtt-server-spring-boot-starter/pom.xml create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionDetector.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionLazyFilter.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerFunctionListener.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/MqttServerTemplate.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerConfiguration.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerLauncher.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerMetrics.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerMetricsConfiguration.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/config/MqttServerProperties.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/MqttClientOfflineEvent.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/MqttClientOnlineEvent.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/java/org/dromara/mica/mqtt/spring/server/event/SpringEventMqttConnectStatusListener.java create mode 100644 starter/mica-mqtt-server-spring-boot-starter/src/main/moditect/module-info.java create mode 100644 starter/pom.xml 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 0000000000000000000000000000000000000000..cba946ca039f556fae4355461fd55f66ba65f19c GIT binary patch literal 45580 zcmbTdXIN8f)GZoBKtYUxbRk<&Q4ylj6d|!8Dq?I%6XLeeB47cD5VBOHx2MP4g@|6RFqrJTH?yu6&8{F+s(6%^O5L9boAMp+rXUR@Qv zPHnxivZ}VKnuey9mKIt?M^{HvS6x#}^Y=|;kW1y|E-Evl+Ewy^QR78EG8~gF?y3A;0$b5B|@W%nDiLM=GpZt*C^YP_hoSLgwFJFDJKh zC31EsavZgCy`0LXJ%{8~&;F^f`PPQL4<2W&(m(v6TJ2mrxW(kk?T4!s)sa?cZ{23F zeaB8yGjof5`z?{Kh)63iEPC($@Ao9K7ut(t$kx|hx zPm=ylPDy?GEbV3XtDM}t*Kgi_EGjN3E&KGjyr#CUzM=7J)3=V!uI?UIZ(l!$%lq+j zbZmTLQZPF=zaU(MmX?3FO9mzTAI=*tw-9mV&zKNl?uPxC9@(3dCRU}DYt2l zyvm`o3V+^G-Msg~stt!9XMLz%t#5J;RJ(G!T~U3DDQBzTchmlD+5fwSJ^cUGvj1+_ z|7ur1YK^Q6GI+A$O{?uaZCFAu9xq-)h^_Wxb zX(*Q+CU(f7--pvvdwxH~W&`T%@Arw%q5obvH1*sg_4e}b=YJ0M_Cmc(y19^ja{+l| z((UDrHW`}@$V2{pKSREYIrYEY!hUUDmF0cK?Jw^u9upN8pHUbk^3_B(6cXskKg4A# zll>blmZ|C9LE5hUZ;u|7-^6zhk)o9Mh%NzbY%U;Ht(Br0yz$=XrQ?lJ%<28trKp!9 z^iC-%3crrB6)fT`34-Eu`2ls;!e5o$|L0rld?xxI4CGDgXTuRvl&Tap@&W^!*osYC z`TkPW5g=NM0tsj-N{e15MR7g@9_(eb4+h$qD&F}kN#p^ymXB6=W_y?SOrT2V7Gf8IcZN z`Wf99-bRIc0M&;M1GUEeDN6AQ>0BHLt#92bebfwj;L#%l{YXk2{yjg%+m9?M~{I` zfu>rB4c7(%%Vzmej@LwS%?B}=#1j98#MogKXi+Z%tgSDP3&sjB?0w()N6bXfk2yG$ zG!4i?G(tB;fzw#kGyTgfqzWhh?C6`(raj@#&;lJLMLk3YqAB;16xCZFi1d0EJx08m z+oJJ>UG`Mq7;W6oKG|tych73}?NVHJK>k6@ZEC-}6%-4KI5QD+hOSSU(84k8(O+(% z34^Dct>{N)+%%?J1hN+dm}yS-6lOi;?2Tn{s>Y5zT`q?AZf^Utquf87U5u?^$5|Hq zN*>8L_G@Zms*Fb3~u2^O{Ok_-E*0~>$Jewz~ zK=kvL5qWoKXPz%uf%uFIl6O{aTmlOICygFmx#R7L+{wQ#9aQ01j=ZoTJ5G0s&WUv) zU(o?C+!}##8MVA}?{Ph)5UuM2<)`=82ebIdB;V#rm!eqRWpMlk88AMNPY|8+QI17N zX?M>^LcBqmB>Oy2dkXH6qUJ3iniRDXl%fP_%ZBjk>adYS9pqX`*{s>3MIT>^8tcJ} zew@_E_^IXPQ9H| zH>D-g%2 zYAQ&ocPR|#7VI=S-FdwHQ&C`fbj-;QMTUw?Xd%+=0#~jrU&lC{=dVe4>tvfvxViZ+ z+giU!g~Yj@N&wdM6edVfSs?@|s=ChX7xN==dPv~N(P(@oKEbor?r0@gUg{C?XH>3S z`#PVg2$Uu^v?6PG4Hf6zZ}Fg#82HM$HS8i&YzxjFUO77ETiTxyDaiAN+RDgyR zv!p1V*#L6$W6U4p^B9p@0n&pXXBiXVHs&j`ozNcX$LK(YkttKTJ}6*HZ*;xw+EKVz zW~mB+Arq0S6!pJw$V-ZfcqTz^FzzqF!>c*JZ!MIf#1{Bz);Gh&$ zqw>5IP0%p_|DGE3hDMiJxx`-dp4d`h61}g{dC#yT$^nxoo zI>+_Y932O-)&*R_YEGK#P+|+TJPC_L43B7nv}PahXKN(^H%2vCgxgIU+W~Nm?P`P##`h_?a(O0d%lOF^DmtawpD(Pl_*mS zAP@~XP`61@?;Tihw2!z>@(B1b6VNJ>9|p!tGuIG*aTGxV1V@4Ygwb{& zhFEM-Lwlt6M{!$a(Ca}L_rV&!P3xU1r;7GX8a~38VWxS=70NU4>Rc&G5e5B4?ZlKc zNm0+3bxg?McTaTNwf576=^s48&OwpyTrC#;c4dV#=bh;7tr7TQpoS3Zz864~H^aYp zXYKCXuCX(CPzTqBft)F7F`&(9E%$x@0z#k>sa6r_Wu<%j2}CQxdJz`HDCjBQ3GRJ8C1j_cyr@& zQ8XW4qN3yb@g9vs^z36h`>03X@eGvfvR}LwT}vM|`lJ%=YyuHmQWoLmTXW*TZ{J?LRaIAN)rYyzJD_Q^~3kAnjz7e8)Hy3;d8<1 z&m+vfkb1psU_k-hiFAK z$(l4etb|N@#d-n2F8~!>p%D6nx!&fkv|n?lK9_|PPbVMpeDq*o_&)Bj6!j>-)^-&6 zk7ny2WW+GGNj^|Ie; z@UyxhW79#RgW{utnFh%b0u;;34#}<0ZnClq{%}yOvPE3SF2G_TA(i zSBQQR{0g?T(ED_g{VqpNPcyuxzS6&&dtA7di5^ZTiF6OQwdM_12i)g{7QM^HpCu*_ zn@IAQUb+Xk-Etp5`pI$C1@O)5sL*FGIc(7SYkAyE{@Rq8%Kf?#ol?4ZEae8OGDo4<5waKI72L zB!C;R=^*9^wb|WJfKFg~#4FUnySmutx;?!uE?LGc(0%AM;0k-m1W=Nqs<3c4@z8$} z+A|72Z26m{s2!RD4N2rK=KLi!A)DWd#B0abNX%WJ&C#R(Cp#_2`}!K5>jADdOAegq z9IdJdnX-QGS~#~TMJ1!C$LIKNv>N;pmbw;!o0dE{!NpU&6Ct#~{3dcD(&7%*pI{lM zHDMYyh0gt&s($fD%&LVD508-5Y8USPEwkt;-k<~92Y14?>j0N#r00l5_g&P6M zhLShP9pt5`B==(E?*GX+*|YxIW0D90{BXPMQ-~!}g*=*TVI1^&*re;ltWJm|X!^zB z=|Q!edg@)i|4B(>55*4btUKcd89K7&56qNBc6xNWPuSTpe|nWBgy!?u1BN4*u*yB) zSS=ml;LqZl0F{1ZjcYU{ zM(L@sfXm8$ZR85u${s(;exErT-DzLLZ^I|7CH5$aE{Nuh=7TzQ!433RWNNEZ>e&+4 zW3GAE9L|VDj@E)~Zs=kIHa2zGirG;x-}LKc#J1A1yVr?3%}y?&dx>B4RzZmZbbYXA zym~kHtn1Ru!~?b z0gFEbHhv*0UjePCE5VY8sMOJgLh83YOwY%#sSN%LzPGa$*nnH>#awS7c#)SoWVh*V zS;ER|a_Fj1ebFtTDS3*yuz)bNY#SFz%#YO~b?DDTPlO*HORJU?Q0zIaI&iaPYlL`f z!Nl^ff~_t2Mk%zLWh(FIB6CppTE(iA*PuZ>Fa@)hw1etf2h!`A$_QCbtTxwE?7(Jq z__cf-ymZMjY00rMSRsLGCRQs+X*||###X4O9_6nOYntl=hFv5|&=D|}6I9oFAJ&DE z1Ul8r{!x@8`A)G_go|CW8qcZ-C8*2P7Iw)@6xED!9k;fdvLk2WF8j|XGp(hMpM9jc zbp8&^Mt4StiKg*Nr@#i<+r|(t)-Kl3gh<`|4xBb*D7@EJ>DM1wYlu-AAknV>Aw^vu zjQn%;Ob-KxEP^G(;w~VA7Z2P2KM*G3aRdXMYBo)h#~o&^WxFWiUlNIS!kFK~Y%ch*)lw;kk09 zPxZcm8{-o*GtuAi&~}zMLoZBCY~Q`{YUU0TK2=g*0lvjhVvsza8j_kZ)vhA#dn zGOZu_yH}GGGE85fl2x!X9$(uOcrE*>%Pb-ikCX`d$Zz=(8?M z2;-`iqV!V}nA5r>@wn`~IU*&j4Qad&)Z_~+Bo7ObDeX9loyJH}1`l2#ux>er;Cu-+ zgZzIJLncBD=^}+8DQYKb8b6YanRT|>Aw^w|KnU^_w_j9-5MhuOZFNWxTT96DvV8IK z@x*D(0aw$fOI7(l@X$qI9WZ`WfPs~AGtt@1k&JA7G5i*obWi`y4Nf4ar6dD*$L~)J z-GqNuK<_oEao-Nk@Ud^)NL1bK>C2&tfpdPoE@nexHcm|NOzYDA_ychDYH0Uq?V<7q$tc1+O--m-4c_1oUUh&|Qx6eh_?|AZvNN{J&Ojf8)M49<4@ zZz-<$+;L2goNN;Id@&RK`G|p!*hs(<|IJ1?a<4nmfwqp5glTUcQ59i_zA#(hS9a^& zq|w{KMqxC|IP+>^57UC;363?U64zTDx#d41@pDSQP<}n|qi=zzA2kTc3Z$s>km|cB zjb*pcUrOMV|$!f4H-0HvxL8&$qBAzVkD1K_Oci^!3 zThA09R2@_VPBwEJYeHSW1TD@@fIjknESgWH*gx4t>tHzJL3@O;AYrQM7?Og5YWN05 z#RmcF;ewk(K)gl$vdz$(O8!cV$Hj5LklGxc|Ay-yr>E6T(WY*2CkZ3SbUB{)dOxGN zLvd~LYN=w?q8f9{U;k)9hP{T3w{UDkKbqB@o0~py*B0xupb*xO9%~DGIT7168mg^tH zth?A69#G7p+<7^kbbLH5eBL~+Y4LBX7+Me0Igt5)_OUF|Peu987phX{&(vQzbrDu@ zNeelQGc%o+4Ov|57UQ$Y+AN2UAJay^g^()0CcV0zZP;44eZ|ZdBri5R0_h4)5;VN2 z#>}pMyB)h+l%h85{B@n0JwZGve@W28)GH?T>j={Y%45Bzgedo&T?PZAdtIo4FWF9# zOu(Cs9x`H-36$#7Ci+K`s4zEK87y zE?|zl0A{^Yr6}QEM9Ue^4~CkniL;M~RN+R1!ORyj&oZ>U@X8cx&aonhD4#4J(^>R! zg8$j7Ut9(f#INogK?vX|USgPueuJ!g2(wzOAw~U3N|c}+$5u!tC3}!b_t49wF$$+e zwqzaY@Hg9W-XSCHfRpxN+*uYTo=QbBu@4Xemxm30qnWD_Dp+AU8o?ae2!2_Nki4T+ zp$H$ALtyB!=NDi>5-1KQ8%&ps0Gi{>lEsJE1-v5Z;>Gr1+*09ml&oU4Im|6iyCEdapX2|IxxAj9iHMdl+mZLN@9bcd zI6{)=$9pefik$)MU`cm@C$M;Qw__gSMIO6qTna*$VMM6C{FctiQ+l%{VNdW2SHENZ-E#`!4TjRinvW-P;a&5Jx&dp;wIDX7e|$pbo(1?_sO-qzc6S$=dOm# zT_le=53;9T1BSufEW9UEnIZ=UFpIGf3qy8idWv61yitoEZ8b0fmZuD>*8=`$gPH3U z#QMl`h`8tLD+#Yo=K~9!aMf$sJ}K%NacPgPAzVerKcge-{i67VeA7;cu+YTWMHiYbtpWiVEY@=r|M_%D)50L)=I>TG%=r5^qQq(tGr5WC|2!+Pa{H6y@)Q+kqbZ?bvALy zNDqOD_ZOJo?^|_X5!S+uZ>=tYo<13;_0nG{`vwhD7n^o3F~5~3LI#QAjN}b7RplfgLi@%)j zIeqa+`m{*jj|WfFBJlsUI%PBe$Lhs^ApQYy2+aGgOK@9P0ksv~zY8>!{XaO_R2!2sgjoZO z-xLtWG$6+Uin1h4Yy#>ycQ2sVP6fYswfy|-mx(*Yz85mlw-5-EeIXc82I+O*f_1kc zqjI!_dIJ6juS(euM)LT1vEp-O_Ib75R<_ekqZa?XD_LGFn7Bp4VVE5`Pn=R_hwVJb zg3k^t?*`HjA{0cZ#Vl_W+euM>F>CHyBGdTceX(NWpkS30Ri+A?(m)XNfcgTNq+c*_ z)g5G5f6*c^^ZUsE1z`bV1pBjU5eP$?tt<2UddiyXhChEN?kN27f*_H9RRGk`v~b3K zSoq1zy6K`hvf2-fBK%zu%Sq@#rFgX z4S+U%y0*~UGBh!=L~G!P?_qXO|3epXZ31of+kIR33ZW-P`Av zE4raaT*+JNQK+N$vpA8pL_HtIN>-;M++A`(uAsmo%lO&G?wJR)B78V*B{|HYn{uFV zvOjF{rcrThNYT#={yq>!$N~(7Q-V{pA#DJDTzgP2b%gnVx)+iY*byKJuLzmbt0IyP zxF0Xwt7UwbP`*!!3WSkrt7A1YaaaN32A&@gynfwj^}%A`^7>!H`x<`|H%zlocVnccNUcJm1^w=L5W#L%7wj7(g$LmSZtm_{$CN!#Lx_bwJH!-NJOIeT{v0 zmjPBVN`UUqg9%I*1TEt8=INcp7(hjA4W@H`HpYohl^Nw7+I0)xmvrZsQ9bn#d4Rhu zTX3HDN${QSO(NX`g8VuT`7b)^Tx235lpk%$#Ek?q`M~^66e8@$TB^fyBbIk&}NQ%1ANKLzQZ||?zmb?8-3!Zxzh!>^_ zJioLkICKOktDfq5+2wccmuJj78v^xWzNnuhJ` zrWKMd??OuJ6!FII+!p_%cOEa&MDouLQVa^|y_I}Kb2{54MNMD(jC>j50|htN1^b6d zQN9a;i|;!_!|ViCiS|E?NBdKC@_%6=gFJB>GTkI`W%%W7!!HqKhY+c^S#Ub1a1dhF zov`NDrS5w3)AyKv&3!CT`-Lh8pv7A}f77yg+-S1!{eD>Qu&E%IffnPCl;>GiZVD4sDnu zEM|KQmW<&`lAvzD!?hEfC;8G#e$xL6xPJFj@J);yg(N@P;P^b zjaaSma%RMQvq{vs?5C-Qc9N(`CGIkP0iEkR7EkV4QCGN z&D6Q;b<%ek75GuVo}0c+pl^^_N*Cb2VASU639yl%G<76Vv(Lu!-QeW9$)>zZ?{02S zzuh|K*oJu@S%=?9ZsoF~FAMx*7Tnxd6?ffCWhtBQU!)%uEb`Be%;@@1w?V6O$F)mJ zOA1Y4Rl5mc)HyCMu*_@>w-OATnYMmC1hKiM`30_|DAV4{?G5`kB7#7%_z0qiHpT`P zDuLgjRu& z8Fn}|N{ZS_e@P(-Opb%}FU_3SkNyKtmPi7 zdYStni-HpzE66)lf3>o6KKj>Y|3znYMug-o-Uq#kaud{xdTI;+31b6&AO_B*n&QGCGT9_0f2 zBm9J_pWzlrV@uwYlsxtv_#~MzNt-3cw~05EKzo7#&yd7bCwQD)z*Rlv7dr8FelxUh zPMS?b=cz`$ja>=TGS@BOFyi0L4+`QJKqT{DcAUBPeBJO`BqDomt$@m^X)1{K$nCFm z{^nn&9EXt#v(>#3|JBrbm!Jogc06m@qpayl{4LV0oXOxv=8HF*ONq%Z@1#vrE#MBS z8l;hrl#n*Hmmqm8f1kUTd#i5Z$t9ged=WMlx8H3FgRweLFp)-$k(84%D13w}`jW)(lM&9dlhh?NIc+ zbLQ*4U!@U2%)f$8gzOj=1Gcap1Df2NV$kiPR|@uqu?mq;n7^pUiXv zi<}0q85~ukpR{y%q0}Y_(Gvc^b527C@bel7akbH&VqVrS6CO{5YZi(BoKdJNPpJ?& zdj|)U7v|p*oF}|blmsk3cCS@*L`ux>@d@IqJc2gV4686zC(9cjU%k<;)YO0KPE597 zM%V~q;&7@+#$A%~pulxfHGg92_5h`z35m=uACd6&96!6CJOx+xAZZf*h}X*))DE3r zJ$H-VR=BN4(i!88dXlPwUc{*}OuR}g$s-75^t2HY`QwGjltJMT--QP&>VtGR z+3HUYCnY>-Ql7hqzM)xEg^tD)J!v+pX_`D5|M?5Ss7X}CLaBF7BG z$?(xLfnh@NMYJmIo_Npe!hOEU#)n$f=KZ6s9KFG?krh+$AEjxt81#g&ebDxLcVaWT z8A?Mfl?e?$LH$VG*OGFMs2v*r?uy8e9J9KcGQ_SOb?ma7sli6*ZD)D(h!2gpDV^`~ zIHNcEtNWUVZc*A26V}rJ-z-C}b&<35EUE_OYzK8G#h^$aO#U;-`}*CYkB4rRQ8)ip zfO7DUWXe%>_d@@WU2EL6eet7kPs+Z6qx+X`U$@3<*9!=-cn_Mkj-wzbmZ3ocJX-(m zkNDs*YPEcg)htC7Yw+Vo@KM0ZN=In%{Br*Pg4vmDKTdaBAY+ZmP%%&5@yBIb@ zpRYxqWF?Ukx%4`k%J>tn7h45|6>oY?(0e<6O}yCKOne8df-#d!CFXT6@%n;6mqGUl zp`+rgtEBVO9(qF}*MBh{wI|4*-bIL@DN;3ki0>mZxUMrRy&Qwf#G4oIl>8`VnvWy^ zHH_WRCea?KjF%Grc9NmjQ`~|xvgG*YxV90tcradezGrcRJe2s&Efr2 zC19kcQB3+V0|iCa6PLBbNJyZhf$Vtp%E}|)f%+7jo?va3XNdI>v)_S#;EckAXChUN zvX}VK5-9@1I%K8Vnf$_}t;30-+0MsiwGLA!GOsPp)1Tr!i?||M)Z#6;EBb9UXXX35 zMgCE%+FU~E|D5tARbtb77`>1d!cdFA0l3~8ftVBeg&M*f$bySWz^^WzQG%`r8+y2` zi>`gwD#jhIKEY2N#9*my`i;BA$B2-JX-|4J{wn$=yyApGC(QaR9%}KOUCJ<=V#`zh zgaSRm#ls~6LOo_x|J0ejqp2T!T)NhMFY?tI{AfNsW|k^45^rT8+)S?$Bu3G1%wRDN zY4i48uHKF@o7fY?XWBF2g_XV{=Sg?;$4?2vp{{jzTx@qS1GPpo(Y*vTF(XrA3ja_=I5b5#DF208>ld(^EX4(CMpNP~dlb<5)=m zEDQc*Pn*1RCs%N--W2weAFj_EFpk;pym{=o$niU|IIrGKm4|6e70@Gnu|_xg`;|h= zFd;uP(LL5CmpY4qaF^4cxkeZJYlZ>_?)hQqV@?-THMbN;4l{6)B8J(kV9cXY$U%T> zZq?Ah+;Ee1ici=#6*fgYDz*WQ-v|ta2mDu#r<%91ebTJ|KHC(ycx;8mJiP@-z!wF@ zFjjgmo3W$KLUD_t(~i+LdEkvqbUVFI-?h8aRn zgNfZO*C^J|0C%Qe)Ob^@0p7WuB4A{hTgHf9MP#3|HSlf-bKTV%Yr~eNXn6X#+n)Jbkimv z8D7oNCyl#yGOvm7&kpJ>fK%L*Dv<|7ht-JnV-jVY}!Pd@!CD=gp6Hk?;&wW3Zg@ube0#YhkP4PqO`tjW6f{Vk$4I_hf0izC~ zb(a^;6JIC`4C|JI_`R2VDsL6mbEXJ7^5wQ&KSQpUy0Qr;7cm1^9zA@aHDXu~2q&r@ zUG#oq;#L?>ZO5nnT+CdH=(*Kau-x5lt6PwdIbuWfs0=y+jq%ViG}SATrwbytJyu(T z?ha$_mOLuxKjaVbV5eZm3b@9lA)kxxi&;db-GP?F;1p}8M@2@iFXO^Sk2hjB0jAoX z#9_x+;lY>wiuk(d$j7<0SpzoeFIf-IF1n#`RpTo@AR`ncRzf_@OhldqEz$5E;ySn! zqg;sxQ-#spR_i%I6`__r{e8)F-D{z(DpjF@B5iUAkD4gJ);$qB6bk|_w&w2aFD{jqBSXbr_>N`nDqxgQ_@sf7t z4L86Zr0B5W*nt}5|N=@pO`?(^v;_h_*4z2+y-FKL% zgAm%KDIT_wt|7kJ6_DPlaU|Eq_?c=gx_6V;qG2eh6lC#}1r{~Hs(0|li8RLujBRmo zD9-#->9gF~)(-z*2~sHtKMykDXyzlt4B(6KJjch-n5*QOX$5f|`LGvN*OeT8Ii(#@ z>8GaxGd%bF6_(d=A15nGLF{C%JcWivFfVo+Bc>%_qOjI^Uwql7g4X`dk-RW?wfJDW zslP(gnh4}V`q)%Fi3FVOzwhDfdl*ALJox^iTVn$N`*{_(gC zpIEr>!uQVgwXyd+7tVH^WJyFxq$+* zdh!T1o~dc1QI~cxnoHC;v|6OXK2bj9tyJ+1S!piBFiT%yqR3s0>nPc$Jv`f4pBA8c zG&o-=CllSB7zfB&Z4xkRmZ|Yv(U3dh?QZ$U_SCbp&yZB!muTkC!PfYrDO%t(k$++3)T>dvL=8yw@#c$yp3cCb0*05I*y)1jOb5HlbifX-nyR8VD+!>QC+A5}i z=FuXY5)e&O_4QrI?J~xAd=% z7`x+BD2O9za|L5o&J}@DY?f+2eHc zm92Tbjegf;i*B4fs55`o26cyFRUx(ckG=iDg8i?VE)OB0oGD66n(+g zym*S|tMT)Mw+c>l%+GNa^5IRl+rbnGwTx3#-q*YA6=HO6f=AklNto4p3cc;QQl5jjx0z?p7T5@MG) z{fTunwJEZh=tr8IiFRqMHNw=MxgIM%?|=Q>8`LUd2=xG$SvQ|jw;@8mPe_da0@>WJtJ+zqJUlqmuJ3Xl)` zP&&aLr<5!TEIRP(#`1diTZoa;m8-zt%FDoylqJ5KceR(TEA%ZKZNE(RyRdmFGuz}7 zyndjk5bk3l(GD6DzO-x%6R*#oash;HA9Z4nTjZ?_YY8UCm(U*dCr(e{!x@?u5lqhY z+zfxYjFj*jkA0$V$Gy0ztofacr;o^dFc7N_j9j#kqI}3~unTc=@yvd)*W$FtiK`iE zhei_7gTt4+drsW^GlX?9busRHZMfP}22z{A@E`4 zb_Hx4@Pe_W-YxvdC}U6%dq|ven7!7c*28Zpk%!M zT2jQ$P{WDVSTuIpdfPLN8V8N{x7E#QOlV+uCtX~Ur*lZl5AwB_KMa1LP3kCWk zzKf3vseia>B6af4+TAv5GiRe8)2>CyOr$}E+`G|&*rT!b7&X4tdBuMUd+g|Th5)lH z5Ji1UQ4vHG2uMQj4$67%qtXP?#nLn_MzbiV)_mC7@CUvByzn@56^WmrT~;Jc1)u7B zSSRWao5~8?#Ib!755!Zo3_bEyVJxGqIQ~e@a!|r6@WXS>_`m0r+!$XrI*uFg!#Ej0 z1My?-8_ta4_+okCE7skAQc9yge|6q2`xQU(95ed|c>u221B}Zv&Pp;Dyyr0kw1%{n zwCqdHQw3IbJAa*XR1-!{ZymvEiSb}i?2SraPQj6*pFT0;xe0<>kIOKvY64_K*1dTV z0Bui<@-sV&?!78F>VIdHlk{zEdJ%-U@ZU;O@IL-gOf8(H9S6U`d3?9iwr6Hs$wT>L zgN65RpID%8K>#l%j&ct~WQ5mt!1ysn8x+FD_RY8m!d%6smEfsmcHcjdBJJTV>EFb1 zlxopU@eU}F*BZC1yZEqDE1z`6bs}6ZWW!Kc8fun^rVi57DMlkh2DNDyGdSTT9n+UT zK5F&U-*f!$?c<^Or&v6;I%3H-*#9 zlA)%9m@H;?md zK!qIRoGFk!mbyL`C{~}NatOrWuacO6coVL{v>=p=#(10Kwst2GKb0jcx|F##C0|^% zZ)lr6#f_V&MzJr42J*?aHSkV>PiPlXChkNwR1jsY^aY)@%YKh} z`vZ^v(OKv+ikWWeF&}IWs|ZdLAOh~nc}SaSpApDO7IIBa!OWt?Klx7GBNcL__ ziWA_Y;C+yO9kZ9ZM?gq0P5GnhD^9)5&+07C+OW2C zuG$}$9jN>2_Ml&H9KY<2BZ~%}1(tRWqY+{3=x>)&3Jxi0HHCmdk_cb?{OvXc7OVid zzJu#=oAK(n&F&NMADuZ-6L!9w+H!yM)3m9h)Z}ph6tEw$=eI(QFoa}PyzY5oJ?7D{ zGE~9QthC^H85i!#etzlQ-MOdEPmK+~rVCXh&k-jGu?GN27EqhQsBIl)oinnlgce1y z%d56BFHgue8{BcL>>r1_gIW3SA$?Gv2u*OU&)a}KML83T>gvt6TVF5bzd-oKuI?#5 zV(|aa`lVG~tRe`CF|}5`=w^4Sy>OG=a{$bu^7Vxtw)Ine&{1dVlkm|GRV3W>3r3@S2lwO^XhVZ+2C;1EP zNUEm5w&nz5qmOXWD67R&H}>Vp_ODkG{n+VX=33j4nZ_10&mzK1Im8AWF3It}`y})k zHgJzh!9UEpH$N+SEE1U>^hby>fpRsFJEKN6IbYH3dBIuN|5ex~4W;3VRvEDc*jS5^ zpU^{t&s7iQxLj&BJ$%nv3pqM?RMF*898b*UO_C!)4{8gJ*&%03g2A*SunJq zr3qLr;>!k;5f6s_i1;dElga;%#hH_X(Q(aS^QjJmHeZ;dq*sV^_m38LUfVMh@3%#5 zT)5kw71e=Yfjq%k$S}jwJPu=L_zD1$V!eo_lRi*Owho_qi*Ct;*22CN3Ld=GrP-w2t zqD5MzqVuDR=bomn`C9u=BVz4eZFcp>7k8&2oAnseY1y5UTf}5`$=b(n$Qr_-E{aW| z>*`%*y2o@qdXB^Xdxx1$_fKpVF%2EZ=;94pD<)`bk>bnDEV5lqWJm28UIn#+*D1Q0 z$s=}36?S$K>%whwW8UQ(&nP@VHiVSv?Gk)XO+S%@vniZS z&ddyo2-y$F6CDBCfMZBPamrNYm%A(!`3kNnH)Sf~wmA)wmUDCV2v!DBb(grua~;A+ z_lAiA7ix)ZHTxqF7syV;=|TLppE>g#ekB$Dss%}v2>+R-{G@$qoFe`qc7W(y1i5j$ zN(2`1zkGk0X&R~5y9~>PQ*t~IMWnq%Bm>@R6q_JRDRe%BDA}-Gc9aP1P;Z6KzgUad zvJc(E&!3<_gV)1f^(>)gaERi`p{qhZ-@6NQif7FfGOiEOe$|lXv$?ct`U=YR_IJhf zjV?ojHrIE23ir+PI_<-%C59tOp#4Y@$6wynZD8W%J>$cJCp&lFMRY>*K6sd>B-Y70 z!a)R0MW~!RW*g-_+p_X*oqFtI=%|qH2I+`SLIfUzp6^5kj&uif<{mEkhc#gS{?0UM zE|J526hPZZ9o$W}sJq*7$-5k(4WjOEj?;OGPtoBx#FGEs=HLB|)xY;Q&dwwI8<89Qwy|>tX==cDAV?fTdblB< z->T#~NYRue&*xi{jWc2OwywcanwYc(sAW_t2){_OaofaN(BrHLoE)>0&-cIK@-w_X zt)-1uMaD+}BxXED)ygvmlC4`VOfpWSU&?((E)^=Nnx4xj2%9&R$rBOaZpIpj4W=~L zYRf-(H?df{{L+1BwQ%zviehQkc1uXUo=}L3L1H>k&Kn&!5(z3wf=>!KfAVh zt#jfpoi_Pyf19B#iO0#457aMvM$hYF9AnRR8fRCO1$A8uTpQo0_7e}DJB1F{Ta8&z zqalv8Hu)#W`mljDDpnE|pdwro65iB^8z|;b`YW=9@ZEce7U~&fob3O$HT1`!vX;&E zY#z}L{>CoC*Z9XWi~7lV4QcM##;=x>>lvbuP7PVTL`DunoDrXT4cJ<6fC-tHUQ0yC_OdoKp1%zgWz-l%Ez4Vn=; zfEIO_ehd=uD(?gWZ*AYKQfWQ*@hW=#z-AK@s@e#mW!bkb?Z6?K$@>fVWpn1C>7G4w z_pkCg*G>ZZ2lcj&(E9F-%+xYBI*}V|acjEx3-Q*^3WjE0Jv*XyZOotzF6pG4gRr28 zcVL3fYgHs7_C!ww^6zvJ|ER| z+JQ%Ha)h!=yO)Y9s>`5sPl)wRiqgSf!^(;6x=QFOWUBr7gM#EKv#%!1XTBi@9gK>1 z{EM5mRxww8{2qR5Dh(y>lROFtLHq~Q{{M%pcaLYfkN?I=rIJ)a5xYuJiCyJXjCCZr zv`PrEN{Gd-avEk!LQd@}A;c=7VwS_mX)DL8B6dMr3HQ;44`elo;|oBvaI)z`fS=)4Vy(wvyz0wg!|ioq!Tu+U%nD*6qf$lwFc$+x<`$Q7i|SRYO;$-i?NPv*notd<4YJPT-KEXeI_ zcU5Gc#BwI5{i&gA%@i@5fylHCD@vk-BbEW=h6G8(f_ zMZ^?Lt~|#46C1K^0euK{*dftpiMk1F?)5ljKKy~y#g|`iRFcsrFdmPaS$GT$swi$s&);o0*Y+rCLU_(Elq%WY_tTfSb=Zg}4eQ?tT6i zSjS2Xbx78CvKi@o_Sdl6<1eik(0;H(cr_nYQR3VEL3Ax#LGwKBQJJ72qbKWg?#--x zz^nvJTSW&Fb~5^|=YD=XK6*yetG+t;irctyTh}1#B4i_cJlD=594Q}g9p&xwU>m7# z5-cimh`Un*l@g@9!;+&bH7>za%Z)30+|IrC^h@@1&80;$8N!MfE%B2&=r40z z;bUE|P_jTtbKsk&&TFOFIkq!zQ@V)CwW&$4U4wguD{lD*Z6C$V+4#laa@J9EPqD9P zJWuG!{aVQ7kh~3uK<5zA8L3*-`1fc)iY_L#kG`c-qB^A_BDyfM+8%Z0?Y|X7kv9!x z6$JYc&w2vJlp+cLMr|1}yo6e9lC$=)SZg0P<*I8~M@_(+~Ey2)Et z6kg1wTo*?Q_SWR(tf&0IQle2a$Awuu`{u4>+dum8z;uKzktuVXwP-p7QlkvKYK=93_4vIgB&3r(1ou%A`c_j!K(jN~eM=JBf^{(Z-Sp zZdVg^6CT;vSl8R2%8k+FU0d)9*uOg)Id<6pGb~L3$7)Mt2(fL$$Ol{9Ax^6$Tm6$& zLy@Dg&R;co_{L5%Ae4@r1^`?_=-dOehV(d3EYeCQa(DLl6j;zoO!>F1>t4du0kjsL*53}54dBlJikJ~22z;01Pl1;}= zxXpOsxgv@9bsv2-uJ4>s0QQ;5(xaH0EF*+v2ez9J(9q*ha5Y^`_5^RFoVHv0R+L+U zak{HxkG!Cx8g;~Bh4qP8t=s~Or5})ygBIr!Jvj03C71peK6>f5%Kt47$-h@sEUT^} zr)XB-|Ev+JinHNK?c&{|^p}qxc<`4A`Af;fvD}q8*){90~ACYtMBwvy;f!(5Vo!M9(<2N_eao3d=NnMbAjR*`1VWAN0`El?hHRO;9xwZG^P79S*meE!t_2lgldNnRKC8w5>@60P@2L7=kizqgI(DKp;wPYuF}K7xW?vnVy*xSu zxxFKNWmFAhwRd7X`-1SswrR4g)ghUg-~YNXN_e8Us0xCiwk=tw=-6mZ%{kxf{klEJ zfqi`bzLGJyso`8RpE^@tG?J2M-JY*p^}c=O-xupRpBp{fftAexe*q8>DGb@xB?tvy z8DHsE9$?hKNC+K~GLMuP2490qZuYFI?$bWHx9X@udJd1E4yx6VMS_XOaQgCJ_t-l=Gi=Jp&+vNZdE50 zMgakWkIvj`{Hl5=3;A%Sj9=3ynCmPJ-~P+D9T?edaYDP2w*mR4z1;GC6UtVM#5eoe zoq9&(H#foP6J&4Ut@Nk6BpjTS_RC0eNUB-~4e(RASl&9btnO1*iE~#+#(8IDLGMzJ zzrNDqdGO8FiRVW=6#B{{+B|-=Cn8|cr5=}Mb-dMjUUV7VNApA3J=Buld(Kw(Df%9g zOV3Ohx7snx>G>M}Omyby5pvxV#RcjETPs}tzC$8)ZdR(>DxK;!HL-u;!fvX~HXANs zY(A;mMp~N}w5Mz9mEF{p=e3sre{f!zP+Cf}cSZiZu#?zvLmn1*gnwq>(lwWGYPU0=?VI%(-tvH}iB=iak6VjwG(Dt!xeSdU zuNg%Wbf+~B<34-&GHA9vejMK|{L~N-WCm{ANbud7`xLREiz9efx@t}Jx=enT*7Z(1 z+gfTT=K)fUU;#NdZwVxBse*5D4=DO>zq2kk9hQfF=e<^>jceu>C*Zv^+Xlk&OgID_heBTBawHC(32CqT3~r?P zYIG&h^{41y#ogCkQrqk`m1POL7G!}L(g|fm1UP#yORPZ+;s0Uoemc(nf-r3MU0>iG zPWD*C7TFT1KiVxDzn_`jf~>KRqYVb-u#Nsx&|;4~*s(0&+@Ub<+LC3-bY2UEuKB-d((N5#{d;Y z3WeZI;GpMmH4O2(O1a%#TTUz-;!P*MKHbDYC$rn@Y0ty`_V|Z-|#>y zCW?-irTnMx{VT#)zW6`x(x3k~&U{+ogvl=58)>MbsqgeX(xXB~D^|e~be7wOQ6^bt zpG;qEjdXf)Yl#{!H8L{i3>j6f420+b!r0w!{=<2S_qw? zf;Ovv`W9b%AMw6Z$}z1Tc&_#UtS5HYOazZ&-NaB54+gCKs1MYNyMU@TJ^L&|TsC|b zG9M#r;T00aPnbbv!|g1O3!;sz{YO2Cv^Eu^NMwAB@fbV_WIY%MEa#B!(TuzTbu2e` z=pXBd8=j*?qT66>!!hQgF$N%l$gKfE)Z+mmH^hWfuS(w|LZ7SU$yKFxB0~X2v!4d%b|+TdC0sN3WzX?xM!<|^PX;&uXngwUR!H3c%hNzkn|G6{7t7BsryyrGGgdY~ zS34}D(6>X5T+8+|2*a^l>s%+K#6+dq;m5+$f8D1iJ3YrNn~C>{m3KiOmuEFQ+*)Uw zhJDNay6j};jfov*IHPYm`TFc1F@CbCbAA>#Q1UsH$cB&6Y5dm+r6OFOA@fzO#UXcE z)w9oeb?4WhhHBag`cRt03bJn&g|tBKezPI6K`6tM{Eo}4~{DYRCs zrhWyDVQSy9)g91rsIR5hp5b$7XWwCGt53T?o}xbPL~GjN&ITLGP?`sBPj*#ep2xU` z>+rt-in8RI_aT%;x7+VFX!yKlV9P?eMnxksLF!57uGH#$jnZ~{Ci?a33u&L3a& zc2*qn82q_X;NcVei{9=& z`4(Jw8~<`Qua?zdPxTyi6nh@uT5rx+_J|Bb4NZT9so+(V6RU`+R=RoglCqQ7vYWPh z-sgwW>KzJPJ%d{BwIf0mm)SHNwUr$2tsJJ?{)yKJ(jQ7B6r5R)q=K3&-7HC@;E z624?EiV&m&gM}8U;pmuzvHB;=DplKyHa)5n5V@1Ml&0V-|HP?cw4zgBdTI#`x8Dq+ ziqj5b8sB-8UP<*Sj8PZ4i6bF*QOcDnCO#-AEF>&I@!l>>A?+R+sVjFTE56-I?z#4TOI&M=yD-s?-b3^! zt%f4JD(B5rbT{;Vo*khFJ^bfDMa>Uypq*HwSd-i#cJ7n6B`HfUS;&ynsf&uAWw;+L)z)Zk17%UCx`f&`lNj`{P zk5Cgv^Yju6a9h7lv3r+M0pkmX3zc7uGOA=Hbodbd06H;zC7&$2pJ|BN5|b1WX+K)E zNA`$#vxB^puRzBx&vIWgxFBHSCU(Wp4F+vdRRG!?AxgJHa&d|whIfGu?>V)aZXgG= z<$5|CDuZpsG26s3CywCbEm&>l!;2cf;`rEDv?hHUE)>9N-D&3%nA8AWR!&fZAhmAT!e;NYfS z&AaBF)-lui7im4-XUf9%2`YJUl%x7U;m_(Xd)nZZPYd z>)l=Qj_!<+)19pB?VR1!#fRo)!^=;89$Xy6UTsLf_;}V`dF5!%bc9S;pyJ{$%t|Cy zn>)EwU1#OA)(pXYACN~|Q;8m--W!EHU5&Bw1EKeauaElRlW!^&D&GstEIn3{O8zy) z_*zDB9?%MFFq%dXS2Pp|ctSUf=HEt2L|0`cH|*yId4DpS)_IV*{dfba9KE2 zQFWf+r5bfxR)+M8Vay+y1krXxxxrl<^)Yu92^?>RT-rZLUU5*x|EB=XD8MVEW4nnn z*Fi1jP)LmyCFvn$*p(QKbxhus(*)D_%Gc{($ZZ#4Jd5pJt~)XMj)d#$%d zr{$ir!!_Xo0BX5QZs5@(*2gX-oR5oeub`1OF#F=5mhoQ3vwD2vh|vRpYL+y?Xs9Mg zeh}|dQOgkRWNi(Y5vR-Y@b5P%D@_F=2Rp{7Eu2L37O7*jNWVFR?LKn(;CY)blZP8h zOi-vm$tn9Ff5rLT$!jiTvY&pC0`6H2x`rK^u8u1uXeUvh5^r_I0v+9^pQ9S&4=X{P z4LNE3la_3g)V%nZ^5=wJ^jaxOw1M-{1D?`5;rTwBQcykrYW%cdMSCea#nCa30TST6 z7!gsy!^iZLC0fK!c{pd;Bm>CQPh9%*ICjn#hm`Yr!huvMxZeCEupBENVm_gMmN0w*}&!iTsRGq7@z>JfIQM`yW`8L40FyD;D zgjJ>Fj+}n}^woi(RaOeNHgZ<`7^(G?Fj9DRp!srpv^e(8E{QbaH;eaNt!Wnkhpx~K zy-v<-7s15vM`_@~nE1Rv6f^>VXjd$Lzr;xjOXq2|$;|PdJk!@5g_LR3-m*cTOJQE` z%m_swm?2%n;5!nVoP?sK4i0qc>iaB5d6&E^@jAQNq~B{4{SH_kNO42}SeQr+dmBRQ z@KJu)!m4_jY4H3`;v?^p2!;B-?{WACmAjk|I?=N9UsKzFtvONl(DhOrvEH&z0JQ8e{ZTo4wsc~19 z|0pVOg1!rXsTZ#(Y<3aleZ7k<==0gcd^e3Y_02#w)vDd&ROQEta-4i5)*owf&IXjM z{ZFA9V}t+tL~+*5TUFTF9%qhPZ;;~eUl{n#Y0Y=d3&!t_4p;n3K=F>g32M;gK9%k& z#(8*YGv^%M-P>*~puY5scS#B_=5&2Igf_z;1F~K0DmX5GUU&@4f?J z+Sn(ia-t?fI=`EmSzqyyN0OstCPH{KIf1jzOfq8l(N!}?wQ=TUGv)QLKi)5Sf>+!3F;pt^0 zwX%2cS@!Vf1!?K+9O-8ususl(ETmTe0@GuLnjtNoq1b-$U7-Jn$px38%Ko?b?ZSJL zUush8ns0d4#f@dHu@=%)pW{pk7iJ$`To*Fb0=L*S7!`Y|S)en39Ptvr1N|DL;qQ`<9>%Hg5+5M6 z33?Qc*lDQBR8~Ldf*n@jyS1Vf@lHY+sTr;QZFmS(T-8^Y(Gal#9fn`Sb3$Jcxus#3 z$eSIF+o&pWL5g{5VP9yCo_|a##Kd zT_IKXm98m1Zdfpt1?%_su~~W)`mWnqRI{u7l!{`_)ew#PU;i@X<
r5lj2SX?CQK#UQn1jFTH=U)J8CVHkpFpH0Sy=>FZR7lA&P-%W5Y^0btL$d6E~U@F2URe+(nT4O2HdV z;X|*&D;s{b^RWWUmv}X%Sr8KBN52KpY=Is+uSbWShhrzbib$eO3*hlqf~IG)nXX3Y zp4#rRuywC;T(WsL(q1cdFvgx6A}jDG`&u zlRp-${pv|W%k4QJPgdlCR=|fb?UN9enq-5Q#=+qqYAW| zl~}8wEp_YMhWUjAQ|aLbk!G#L6X(-P-(6ED@tsS$N~hQ25H!EY<$nj3mjy(TX)8e) zI{pfSRFxhx4zp)&RY`dF(E1IcU|S;zWH>vq_;dG&KlIsf840jJlU^}H5`Ey{v`nL~ z=1ilvh7I~N&y^okoE`cDyXW*;WOi9jpEyRAU)m6(mO4{qbJ!-zsOR1H zMb2l-`{=t)7hyD+7qefaa;iLU*boQmSYWlqOAdXrqbfH?+oQ!{)>OI8F}>Bc3Tm;5 za_wskC(Ma6QVeX6;A$$)KQuledka3onmvZI(!+*w&YGB%GRXgf+(fu>RAHbsWA>4G zoreWk%;2Kx>JvNL*pR814Ot3@x%P=teZyFU^(GQXvza{PP#80&2$Xbp@Y|rL3{AP} zB*7{O2q>UBSuSC$9z}@)p{~Cx>RiZk8ZN(&`Khh24b3l2bDCk$w0O;+raqg_C0UgG}8iASTEVA;oc!yU^`YWiP_# zcC^3EcW=C7fL|Z+z!>uPz!b(Wc|VAi8+aoEuDHv+avNNUBXbVn83AAqe>UyyaPz8v zj~=D&1av|$E<~Q}M*;Qq((g2n9;h9JXAx<|cmIH}!n3oyIdsC0u&r7DG6T2THjpV% zi8E=!;x=#`*{TQ9^+Yp=3K!YAS5E%;ww{H3VlCI;u}Mu2h}Vs-i6Tx8+z?^DaE8#= z7{>!GJ}s`e^`I*w+NS3oa#WY#Tc2KwVSb?3yf0$N z5@taZMpy)_68#|$$)~2!C@SYgYuZvXHB%z!+YvM0d3S9l#1~|R@@@FN&}zB1i2mZW z{TtnL(UeSakfriYdB!4DU>H_BMSjpS(8UeXgvTdEt zV98C(&9k-IC37ituVqh68cZ?QiaC$C<&@^50p~%}(t+N~m`+o`=L(C*YSQGU(;f-A zW6>D**;~fUao^g!Yg4z|`RKo;22kf1atO=yDsF-$FK2~&@9BH>IP!XVY*~hD_=3Iu zw0D|NMSQuBunH=0WtM_(lA?OHCn!hdsit0{Imq_v`JL6tL#-^9gJ`D<$F#xs_`Gsg<;Qdx=nE*cS4=lTPVv;o?jp+W=)a!*aBkGXhEJ=yLgvUwHKb zlH+z|`AHARU%tUhUd&}k*rfUD=WAqSWB@U4bR(Fq!coNK%l}i5;8mV(FnHD%jb z&H6}vfUHEfSgWO=w+kpQg&E0J5ysfinzTBXK}J25WTy6OhuPf93OLp5j0wGXf5;0X ziWnQ<+$~#jFl(tj^l$J*_@8CX`A-y;n^39XIotjju#(nwmmU7FReU8S1^Gvu4D&`r zDSQX{@+>4~I*f|h+WB_reBc5wridfUA*F(8tC*|@v*v{Zh7a%f#a!r*nMaQT`{w@{YdtbV7$7VEAxn7<`K`3h zVDW2><9(%OSnKnR#}p7a@^-_qP@@ zVUz47@~84kaBFrNO}fi1rI!8lb)6E#2bMlymJSJ%o5kusVrX2+VQTJDS)BNh?Clcq zE@%5e=^@{-X=7iL&<8X{o7SGhYo<;>Ct(e%^vf-pBntaWc#buU@)WsxVK+Yha>!;0 zI%DFzvZF+w&;{FOtByaz3PV4rwz$rmZM~XG;f;hIErqcQ#J*6Y=p=s{NB$6QaWTNS z=wqNIr*vm=d0B{D&w&T@d^z^=joS2bP^ZArcnoxeZpr8w>-CKuqS$s}jm{te_&NQ2 z(?bcmSH1(kA1XH2^Ii;Oee0T5r$WkANGW@VX z4Yz)QNjHLeB!L*hA)#A}eESs~>|D(~L17|v z;Jq74msoq_JU^_2y{fBXWiD?hWGd3sC{c4s<`zAOy>F&O2kPBDoWja;b*lA2kU%Cu zkuaqFyGj=4E7OI(4>Q)ZmiEcYjEcf#>~zq&6v+OeZw9JF%Vt@$9Tb1fS7T)KWzFVa zAqR)}%m%e~P9>>D1{c3$+8+8gskxu)>2#Zxl@YA$FZ_zoOtRPY0Vu7Q8 z$ZJnE6n4dyne6S~RfWmg8X#Ep>+I*^^rhdjT3Y+eB#0?%Z>vXc#%ZCd-}j}XJnwYj z-Ysw__;{f*-(~Its6%o3@5^fPj0JGc&FwP$H>+;M@! zo-wW?#^K+tL`H{KV-}~!kS){XRM=`WlJ4l_yU9v)V_VSm>z`Mhbq37QIHRR&$c?WS z+hAD+O}9jOld%QEU%qzgPGzy@f&Tjnsfm1-vBlQB=nXRV;H_f$*L^00zR-8y=}`nb zh9lz^`Pbnt9U|)pauFZ4FdA@J{JMWPUDs9nSs|o$Q?L&UYyF(D>jbbBZLjkq7JT!1M(}H1sHp zpCZ|B^3T^>{3|s3zegX0(G_JS zwwr#!%121eGB-DdoSky)eczXW8^aNY>z1fDWiNn1M%9Qvkz{7$Drt4|I4~LB6{yKp z-2b;Af9Y4O$p0gd3T`4CnwD>#Q)T}vI(5r;x=gvETn7gXGT z3QpAzoA?fohxDwwESD251(Rb-=o@9RRmipYlRbDBtVqqdF6MyPnwH?W`K?6f2=zKB zm5-o-ObuHMQO|Wh9~hl>_SC#LMKsiT8Hp@Zz@Y>f$q~E5zWYBDVq8Er3 z{_3H?@o?XlW!XknPfd3GRGK6GztUA$UBq&tG@El@UisUKdjEd|*ZZEU{Yzq>lwUl= zgG)BK6Mw!pnnSZETJu57+6*Of9p|#w&FR;B1(kz`BUA1?E;;o{ zCg4P9{KA4}UQKeG*lpd+flM`)N%3+WDCMjyX@;)tW&93|lv&IPF%k4czS36sCd!fO zK=E8Cl(-|1u$g(w=gGNuUV#@5&*`r8j6J2R0?mv;FHk(;PM$8$K_BH-_FvoJxj^lL zLs!vkWjP*U4M0AmoCXtq;8dx}l%$|W`2DlO?P=d445)n0{~KD;qgh-ue4mLe_Vm!c(Y)s5t1@i}fO3X)}NqEwe04>G65W8um4)57)# z0{?zh#+(JE@pBe+K^*n%~DBto^ZjsVvWq)_E#Ouz{&fdUvI$zvT1X>sP zMvCQ{_+6sUkXu`gcBvaBxRN?_>&s`C5@uE2xjql!QUdjc`-{e!hqK22DHz=f8?Ak= z29BX%q8>ENWi^vGOcBAmuYI2pJTY&#@zf^Ff;Gq}qiz9F z-*F*h)NQ5f-+J!3^GO_uZ*6{A*R3pAqBK&f$r_C>cOTb2&{neSVt@7r{O;Jq{CpyVHm&YS*H2lcWn4;bkKc>w|^@GHPoP5j3 zutn?#FN_xmQSgJWo7OX~ug`VOMIHA3HU&Fa7-#tfFz-|&8A-klnjXtalsXpUcsi?N zPE4)eB{7r#ePp|ww&<2r^^1m7##nh3eGG~AF|Tsxhg97v$$Y-Sr`=W+xOO|58IWlL zeT}T)U@*d0`XAtevgUClD)t~H=e+WBu;aD}@2SPwt`a?@jOl8s28Bke+D)8)?^IUt z-~!v@Ul1^F?1l3mGcGC=4!2h`a~R^5b?Cnul3vZ{7S*W=C^?0v8Ja+;GPU4bqL$*qj-4LtVPZdIJLH0-p&#`>@7$Fo5> z$(6T`=P3LnUZW<`*MqD5WJ{g@Lxhcg=@1yKgxDW$O5!Ny$TkXPiOp*9QIW#0vGBwO z)1Yzlu0h+u%>NWjBiL>9?VtntoA@bE6d{WR5Vd{s95RqP6v86|Z3~JA+=89~@WRIX znvycC=GHR+TRDW^-yKTYCLdvHZ>lYBeG_0gx~tCNIkEo0>|@3ygt{=D+tr-gQNmEd zIih;@)+_p9E)Cb7Cb-Hwt?&R>t{H2zn9vKWe#Zo0t3oHC<$t2L)~KEv{kq^#Th)Kj zvNGTBA=j6SuXrE$ONrkOj#>N>$R>n`x@!?-@rb|A>jY2nX^+*-OcFcy_k#{u(_;rB0xRRUjs9zMWnMz5X%N6^*=xCxb;XXmwve<4nw z?&xp$k1#T&-dM(#l!)oFT)BbNA4+;L?Y%z5G1^E+lXg2S@_SxurFZA=kXT*PjH7b% z`hvTd=H|4bin(71rk!_{;)1;(2nhHzGlbXM(D%KydUV`lt{<|L>NgA2BMpuc6HFaI zCe1#`{w-dok(lDWt!GmeJN+PWIPH69rD%5@Y{*FL2a$y#{IH26{Oy3kb{~s$mj^9=oVie4_Sqk#Te7?42^>Apl3Ke(a$!TByycG!y_>*mi1+m`XnldZ zK5OIsivn*L+IYTN4D$Jklm!z)rOsb8wXOd0k7QYt0w17+(H|J<^sQ=J2;IymyKexAz7G(J39LvEYr%d8Ur58xMri6y85|VDHxkTT@M6n{>f5T^48GK4J%*SOatyy37{z?gb zfS4G;Ke@^7K_=oUvlvuXyb%cGODer^fZ)Mj5BPu%;^iPOVNl8 zzO~WupHdl7$qU)aAu-hG>#qcy$MJ$rBdap)V@oOcnbqy+uXk;#(}~| zo3S5x-EkUZ^EdX)g&Dm!4&qnVkC@HAEs?yHXf6N~(3Y~vg`aG{0&3&POb}*Ob&g8} zL+KZ*6}FY(i%NlQB5pc29_T*oE9#htZ%3_z@&6M_+`kX@05{{H%SQ~yKo23QElh9z|G&# zqxJ~2Pvt_zq2y)icKI+zF^rSwAy4roQX4zvxgkN?^xP_6o_-m4oC&fLxu$eiAJTs~ zQoaRftlK?eJl2G~__OZG@UM##FMy5GuG@Am-Uo6ha(sC+O3vU$M<*^w<3~2F2#`0r zl1H~eND+Omm2MjSq;J?Er`741=hyMa27uP8mR=JQYC(iuW=9)m?P}8i?bGG&vS|VI z&uu3{`f?P6OVAIAbM<3(jnqJHA<^qH?V9h(s)}a4A#~D@G(bZl)OnE2&Dh$`OIyWq zdMqC0w5Z3&2cLSQoiRC>vohVH?KF|d7$esZwEHz&r_Txx4_-M?EvRU}@mqX5J^L$E zQ4A9pijU8#Y#@%O@9m>jnv1)v|<5n>U z1lFpTqtKD8@?qG-Gq+EiYOQ8iVz{;;s3p1Qah_+@8#_NVqSfS* z)Jze7@f@bvl#A@!+TWyp)rn)8tIR*GH4 zhHbUW%tQ|Yej}|8CNF=k_JZh>>zM)@(rMPL>2_K@)rWsvrslCc^_{R2`cI*mqfUnn zL0!!T+^Xp&SJT@taL>VMVg3IzZQa?cx-6joyz)nmHZq9xfT2RS!lis*BNGtt7~jGx z2QfA)UE}ogdVOwWVsks)SS*aVhn(vK24g*{m8@_#u*G~li9oGRslCB81`z8##)+`G zd$3AWqFEepqH>q-g_ayzF2kM}E*{4{R|5znm(S6Z;H*EwmdEG*Q~1r(sOdp9!yiX~ z$0Aq~rBQT%_}#Y_>&us$&{7>wqKIuQRhR7Wx?+zB!U*#Nd?n@f#9Kov0Pi#bAMgM3 zrpk!EAZ}lKxs4IeCM6*piUOFH7Bzw!N!J~=e^^>!J&?0OoYW4Sk~qAEiQh4nXK3GO z^~z|1-Ci{|UZ(~bUWrBe>xSmDhlxL5J>N)8Lza_bjq&^WbIEWXd`qL$qGn}zXoNT4 z&`pIGMuMSYCI3smy$$HMsT8_7{wx%YoB_PKax)VUarlEPuSWIyjSJq%nbix@zFYjy z^9$&7kdzY3M<%jUR}$|@?TIhFuC#u4GA2a^f8-yf?r1O&3g9CCubOuEpbT0YGv{lQWoi&|Drfw>=%FPL_L^ZlLej`g7P=t$XakI?1rO0(G zPW&u)5qc4-2|aX6A@(8B52=B(|0p-@Mw~1i9k#2k3r~C991(VB^v>VqZ6W9^!fqiv z4Y8+sj>lO)9kt=g>}AhO<(aIm;pOUIV6u&l6V0>pdT-BgVAtVOoY5cU=oer_lsXqX zq}5~!`WijT+iC6b$2}i1b4Ho5nZ%LMBp;jUg|A%#tWdypN_srp5(ZP(9FI3Z(i>|QHw zlpKVt+tI7d(IhsrrtEYXCdZ6^Fz0iB%e%-*JC?P*#yAqp^!{aSG$+vtW-NwkEdAwu zL}-^GQ7*nmLv4p5&WSXrv2D**PLIA>ZF$>EW#{(PzdmN71Apm@HAI_|xLEdy{}en$ z29?)dO?Ev0R?LRaYHs`H7YBf^C$eHdkt26V)u2v)ZkqC`?l6^rMI23aSF9{TCI$Cwe|_ zB|8&|TB14gid$g!4aFX!;`oL`BBhV#=D4}j*1EP`?leQWO`hu>#h*$G(PQLx>=vnt zNUxZJ^EuOt4TcMtL+Q@$o;xSx?iFs6?K8t7%oV6lcpBl$Gq!3*)k{K|`F7;`&rQUJ zP7rX~L{}F2x2PqJ?(0JYa643knX1OV@o+oh4_eG}aUg7s?begv7LCtmjLofWD-9-A z{OG?*^YxR%jX@D(Sc-^<5%!6nH;SsWef*;%3o&k{&1**I_UC>HoAB8nTP=G6ybar> zVIug0D$bUlE9PnRY|O?fu}-t;(Pi`A(_^F|$4y|of#R^;`)jrO{betzuiG38aW@{L4mLySTrIh35E7UZuLB18 zAL(V#k;6Wcmpj25(|ZG61(>;H>j~SWU_oiGRGx%W6d7~jE} zckaZWlmz39n&@kwiJ}0#%{>Pfqu2K86L$|@4cmPedAjozQH$Zf?qFE<9#FG$4SV>O z#@Cp;5MTrh9lZa2Pik|l47OgqWsJ60TWz`(naAotz=3) zFv4kd%Iiq)>=~nX&>D#z1mkf&=o+?rFp|tWoKDEfEI(pvKL6Tlght>mGHnroIj;E( z&EYnpPudXZMrs6m<0C+qLZ^#lj}b)SLNmF1IUy9i6*t|)_8Oh$Sah8q2znkwy(3Fn z2GU{fpefJtf+MYkn>B z%Lo~Kqz;2iM(7C9ErdhR|JB;JheN%te|NS@QiKpHq7qX%treM7k|a%78RuD&9400q zgJGgjB&%9X5tE#99H*g}L(VIb97p3YW+rkPW}@+B4(s={_q+G|?rXo-``7!2YnZvN z89dK(KlgoqKKJKiT@EQ;5>WPQ_N|;AU_V(3{Sfb&T&)?B2{lU9+__WV@sL~2e&92f zybke@wz;1sC+$5+NLbb{CIu}&V25R_1W!o~2bT%=q-JyUI{Et(kP6pct>bJTiG!}A z^>#&$lrsQaTE=O_-)osLOSN1dAk~4&UqdQa1l?n`J)xoHvl(;P#U2{|!fKw-bjUb$ zqp-H`yFK2yzqZsE%oH?I8y@XRr_dKqt&2H{pU8%9rvp!U`7Oi)}p1Adj6{@VunIk3e&NS5YBJB1ij) z6<@FCo43CCA+s!f?{$TLX{AGY07^1R;#vRjSNZA8XX8JL9=*x{SK!sN-)I4EzL$}h z2+l!bGw^H-$i&ajaOGfUK2_YHbPZd;B4y9?2QLC)Wd#8-vE0HJU=%L=g5_@`BY}0d zKHD4rJeqQVcjG$!XF7nSKO+DHu+dUs7@KD$P_B7B<{4;vO+nPj+}+L_nE{frFy=Fpyf%aqznaJ&D>fzT2bhL0%x}AESQk|TniKJ0l}p&V0r^!u=PmojV=R) zm5i?-q>L~$*nh7Zaq2kP!L#+|_aS7bcMXqW@A-G%EJPUZCtQy?vNCVNUEG9q5?-ZL z3Ctdf&GJ3EIkL~6$k{n24%V$63w=*=sRxJmS|dgUmi&?iIW{zV#E>zGq`+&gr=mW| zJO;#|T{6v>O=!g^_s%v-TKmt4!_>%Z;c-@oq)V zxpYppm3>N+R29oJoV**VT7M!h2HhI_i?nZC2ND#{D7&pBEvFt2d+*9cYww>uHMiUV z|NM2ru;^$eL97px^OTgXjN{X%wnjvN(3u�cd^!KvlYN+qy4w5DHg~h}&X7Ncof+ zv)Lt9l}}0nbs{EWQo`0!l)hxzO+L8w9#tbri z;w9@FdWb08b9ZeB1)P7S_AFOG$m4>m0ca-!sVa6COEq>l^Xt8^ufH`6ik%FJvZFBr zkrlTMf^fc)XBH8Bo8~&T#bmPF5sv!E(*QNs&JR(3edROsZ%?R3($0f)=?neYH0ZF3 z_%ZP({4h`yS#|){r-5rdwvsqY7}{;~BT^BR)U0VeMhPtx71QQOk`U5UeW81srpbVG zZ)}Tj+_*P49^I|iQfHJOx(si85DEOUm+)Npd>fY-uerCMOjq&>K<;FDs9&=?tX^T{ z4wnTRPwPHjIaZa4WwkV9Bw@V6bsm>^d>DBebRCBVDZ8(rqvd395=TS(VpVec^jNEq zco8{9uj~;&M##l zuB5*JtZr=uq?U3&HnKy9f9~z67mOjKv((4U=tLc_995uIw<3Q2sQ&MDo}k^id<4o8 zg8257H;A8`?O#QXXn~*p`8rA&5LUqK4<4riV!ofCpS<0%?kpM?~6O1{1KNQH~t}uJ3ikTF=1P^>pw{bT>5C;8jJl_x_HS*9b{d?WvUuZx9I7JWay=846FH0ril%Ug~r@I5_(2d)^ zw)XEGTi*8-OyDeOarJ<`6mF-WfRu={i|`4F2)|n2IRE^)gK9Rs;8yH*em5)Ca-VmK0`Z;rLCBt(= zTstyK-i&zQFZJ%AaX-S$71}PfcR*p;G zv%6tgaMXAFg>}SpnUgP{k@~DKSagV029kf_*8)J0BqMsK=8)-xv%nb^zL5S1Ni%#JC?8i zmp`f&zeH>gMmI}I5{1Fl6p;)NRhZvoUO3kz_gyV`b&(t^xe!4)A};n zo&lV%Xood`*X*pRgBC~X{-~Yr(ws`!%V-0UbJHsH(Yq^9Q>8}|V<%M-mc{DQ_?UTX z`4r>Y2a7;*(qPo?NR8o_B$Fw6HGv1?rakgH$q2(M1~gnLs3UqSb&@mdru=?h{8ckS zbPT62&}$;aVI~$8JzTr6gcX$wLqTtE*TI`9bB+=!O>xGJH$!xiRzxe%A`#WllX+6h0O}f*HZiA#zhU{>EQ(MJ>v19{5<147%P)F}Q1}w%XU&#r8;#LCOs_;)C^3*Lq4GSQJz~&hjJFSHzM< zh9AgWlm^@nvlVyysZCaER7KIXu!6@|mG`VtK7@;3Zxk@>YqA`M48kB)XZPpM2U#l- z$45}EW@5!9E{s2$1Ma(b1X38hQmaK{$6=E!_)aKw;l{vdYxN$%pI`eZ?ov&4ftxY6 zB}YTNb@%%3U`i0!{(x)>r_VX1-$pnY#w#AT3Ef)zNB!BhXx4p`l%S2xQ5kQeo*1je zlo6hkp|JUAY89~{_9kemwOZ>5OAWP*=_mC zAA3f$ve)6-z(mlwRM1y;gePdpm0^r|Ub4C6wGFgk&NsIFt{LffT#|q|L3$wl3Hz$& zw7`CxXuJYqd;qG-{KTWd@2p2sb%3B$B_(P@_53yIlIuWlaANWh^SzTw`Uwh^K30g8 zZ*oKN8uLaW5yxU_dWC7xVwWhlTgrQ7u>w3v6u}XX7Fa-ez=Ls1UM}tF(Ylips8f`l z!$Ny;YBWN;+evDl2a4pXWG#taCYYtgB=LZ@;L>S+dMTySpBYZe+WXBX^yu+2w77Kg z$5AASvn=oaz0OAmwRQm9XRT?A$iW7!c`X47#UIM9a0#TTe}k20upPbz!!OLhuginf zRZA^a2Ekz|&Z$w$zeJS{ZL3SVQ@j_&MTKBH*B05R`N6U=2zziKN2AxXT*{1v65*T< z;DWIYY^G@3H4y8G33&by6u;od6Jl>iRWbtjI$cc1NHsMCJ`lx(-2>L+vsMkJEeONa zdv{)p;^@1dhv_zCGJK0J!6jkqzubn`qnguX1M0?A>AjQ-Fyl7_#Iq)hqV9C!#~m@$ zBNzyaY}Q<}NwJYMT1$;3Na1?Ovt*7CRnB-|VNaV4FjO^bYsbq+t(FQ)C0XMAd?PFV z*w;j|y)*B-?Ppz4@OX>y`n6O14vuLmK%!|xZqNqil&oxb=YufQzm9tr;8sQ8-hTfA z>n>TvKPSEXhfK*%2kYJ2#SbL;%tQ2vTuZ&@BD5)S&2XD!4%ZLU(km`aN!#Q zg*J=sv$w25R3zWNFCp4V!$c0Mbum@CPtZ=_pticW1=ei6TK_gDbl)0hFLE=0pE%H4 z5FOJ%U-~=fORK(=FR71hCoX;EgSvS0Z7udCGEMTR?7Cg)q1YX5x9B0fWzoTKi4p2B z*ywL)hWJn1i8PTU;>VOH_{WFfz}=hM0s7Nxi$EFolT5L6gsy0*1$7B;*7f6zxEdOo-YE+5(0LVh&pm>pqHJ!gaWtlOmA=I2ZV#Xlf2d=TCn?Gj0 za$5lG{?GpdgUQ+%yBBSFMDn;os`PoxwXM`HZ)B#~HEH4QIqgx_B~V=UjF_V0?y}R8 zWSY^K4c0hjilh<>?qlkHqmVe?+OWuY8J+>CATP@C7Zy1ey|k`B4g!bLaA9Z|-%!P< z`mgms^T2o=yN=9H5yk*15Yo8I*tnaA2R-Zho;>}|k0(K&;G^^xNUzyzv*f8sng(Ro z#FS6OT?z2sUWHckA}STkD`ePz&`Y)22Ekjrz|aEjIswkV4{eD69`m%GJqgIno`EOi;w-fo`K5mYIiS-=OL7bSbDbwRf9-=FYHA074Nnf z8PQqsi6P6Q(w>I2_%QuOefN0VKQwS>>lM3QQ3!Y*CTur`yPxl z=mq!?lzZBentLsT#Ry<|r(@x+MVI^l9f@pCTrFrBeL8QaEaYXu1_J5xv5(UtN z%$3m5Fa1i)!gsjLCA!SeP|nExh-dn?MYkap-qr zwWlo)y%C`99<)Cpl^u%Hdpjz1_tTn1FQ2;uS1e#yn&PxZ*pDl)jCb!(rqC z4t(i*aF_YWG;#`n$i|Nd&Jpt8UsdiFqhjEH_J5UTijci(D-Yr6TRsb@$*||16EE##y1I?-p!^=<1wF)qxW3 zAWOr$|8Zc!v!5pa6+zL{q|Zp<4ZygF0_MLz_^?pAS^kMp~L3VNq z==<&cd)=dtmde;a|6Vt61f;ZQ$+0fdc1`V16n&Ay%v#5NvR#a6+l}Ce(b2BS_P5ST zYsH+PlQHg1C)=Oi_)C{ko%vbztH0m5i!ZLNkUkcH_;yR|e#*iJ{3@%V{}|by2iC?t zLRUfhmk2Mtp;%RP#4d5MZmANrS8HX=M-}g5PZx0c?%43}N6Vjn zD753n*Fgrbi37dCPlfPpBvR1);u^324uYfDQ*;=MaFh=6X;u8jvy_lYayvnzyv9r- zYQvcT$fu|U3_geo17r6L-5|T0^l3Ziyk8n+T>P(30DpYb&r(CJ)fW~&7GmL_3&G~E zT>yo8tE`8@04-f(#HPtJ1GPBJ9(OROfXsi0W*-e&A@;n6N7(k zyLpV2a8=qF@M_l=ZR0(pN(;GHgvKFVO6(fTc2~7ql zitI&|0O@|g?c6-KBLTM=XWY9Uw49wkWxI$vMTGQJ+CYQMh!Bte=ao$Q6@rNra8?iy zC~2Yn3=9a_DJdF9zVw`MrA#^Ru~_%p_N8C7j$zU46`sjN(Hjh45p!TknKukt0$JG@Q)7JKKDOgCQD@<;$} zpX7y!{0^LvV1g##HU-yZG#H5R0mkuXPw+;T!n$w5tBcYsP!pjX6y%?#JjXoUpxj-E zZ}!0(x+3wTjvyz199tSeK-|u$3@dWt`wk>$RQsDT z46c9ndJ%$xzvpXlf2%Q~x4;yG0omM?G$enAC{IpHKy3)aY(f)m6U;KaSL_1Sy~T3_ zhc*iIrMtD-iEFxEph59&OX)F4#3dvwa`^W2N4SW*QTLzg?A0{frdZxmC6~ccVR=;@ zO+!#WA0$w3g_GsJwu%r6P_X%7@*9)myLo2Ml=kf1yZ!d}mpgof{(LaTql612T0={D zP))0)n&$~WJCs^vy%2p%%^-Dh)NdTz(Ko0*&d@Icl+7op(rg56OMme4l7(3NecUf))6ULyKp(9uwr^pMamRn-#26g- z&UqrP)b}qhg4NwwQBnBx#${dV?GHV?9+PO!&%npM?@7Zsy$YCb>n?m=ia_tetT>b| z=e4qwaJx7?!~G8Y?4n1@-9Bwfi2GU3{}VobEoP1~EU&FAG+fx;;$Fi>%fUh!#xF0j8 zqS^@Qepq@2+*mVYO8x>IWTf(9urM7d+42*KM6Q8+v%a5f3Tzvt&KZ4JixTvQ0F~Rd zQV$y|KD6;2&0?0hlJxgFZ=Mgb|9cR^o)Ki6b-z~Ep{dpm)e(R$*fWE|h(iJLEzqAZrB9S^n{6KwIh^nNh2(nA zw(bf(EAZ(^A58@T>A~7sv0d1nCJTH&8vM<_YQ2<#g#6xNii$r=UgbPOK2+%2%RSGd zPniE)QAi;PW1)~zz%1UCkJa{QtZAfFpHR;HaNO!`%{bNZ&NcuT{>PVn*PI9DAtj^$ zyBl;fnEi0^MMH~cuO67aSs&4LSZk^C(2%VLwNy@Oiq$m7|5ET)Yo^MjL~Bwfk*K5*qfW z-geN$k2bg0kNebX9jQzeoe^8`84tejr4>24o@dYV^LP5I)I@%uM|b+BW&R$;Q+i&A zvMQOmota%vob+gYdZ1hV%zsCGhOV14h%SYW-@wW`!rqn|xT zFK=OK-OhNsCNo~Q_X{vdBqqYV*3H6A! z0d3u2Ezqgs$rHcoC1EJ##PP8&{TutI%d+KNs{}jZd@E*GS=HLiM1Ro z=?I*L>wPa3TDz0puoN-Bg>tcMoEgWwnF^^J{vg(8FbtwV=zFKipMrbJH&*c!+-1V>$` zFDw#hqf_9wWD=^5ROOn-4J>HZk~;&(&G~gn6N=m*EB6}vUFFh#CE?%eGOp=UZ!_Rp z?LCPKShpT`{GI>-bG$ah5b>d6hN4YciSOIkS7YaKJzJpF998$6EkW80@)pBDr1etQ zIEh`eX#S-D)2Dr@U#g9@`fY$5bkZQIZ);d-i_a}XU6-mu(p{PB=KU-5TH%T~Ikx?E z#ufyA7zVs5$Knoikm~P}1F4JuIS~*`8bA zucE?>@!lSRRAw}U32zi@pDf+{90|R8a>u;naBf@&_2m6KjzOA#)?WDskqz`0{?Ett zAaPNuwJS$((ppBg^zXCRa-`?<|2E)IH)^qA+%BO;x{|Jax&cvutM`*ns3mPeaDKD5 z9AE4E*o9274tH@B_VI&~T_d^=h-o)&DAOhdN%6}t)fwkC{QV%kH}wex=H73xv97;o zFi*eTW48A8OBvOU9T69PxJ-}Gl0lqKnatmHr0nP>FGtL;rtc?KfQk7U+aqvA?Jl5{lht z)>zE2t~ZP25p#r^l1^fTzRri*1(|zT20NlvKP|;gm{*2%!QL4+sQv2N7dF*4@{CgG zjHu9kx)(ez|6;l$~YH%*U@ zEOk5gQD-SEYxTx;8EM10rjoR~Vi`~@Y_JZ?+#Qi>Fh3qXIg}e|GTHqhd!t2PjgixO z3z;oR7{`y_n>9AR{=yG5r*73*Non`ROyONX z^)weaL5(hN2<;4I(IB}@jX|$vYwx%da4@UQb2|B#2 zKfE@g72Tr*6@06=HR^I`6vYg|E#~I*b~3 zNhZ3!`a!`H*y@)L$Ph%}U}k$KXIA$4jfqI!%AV`joUvUuN7SK!uXDd52RNnJwIWcTB}DqptR-&)ACb=ZsYdnI`tFpO4`HC@!M`UvJu;$ zY>=;FF}6dNA4hVfy)|}eM|0+=mkuBFCLV_t&qUt`V$kaDb8BO%Uz5mtNH^d$-<9J{ zGEeFD*6)oVy|yrqViMzPV=izp&m(*{Ek2r`E=;fWJlwZ>Df`;{*DqKyajL=YfrZzk zE1iw#)85I4oDV&|qI9N2=wN*(C;3p}KMdS~3evwGH|m%p!*LSMd5SeSnW>-9HuC^? zdicFGqn~2-`$&eI(M5H)set1>s~YHO-7DeDmgJ@Eh>4DPGV8ustPu^rv#>Zc&_FNx z)a`{n)BE@x!})H6*L#Nu?7zr8DVdC9ng&$uscohhkaMs{Z?Q07r|k?}V@ z`rDYs{F^Kj;L3al2P+B0*JuY?ZlQc@uPNAL0#vo-jWaS-jyxhbxRKWWvi+0zgpy+0 zCkE_*pW0p^qu^=F06Du{eY)P`aZoxEGcJeveVQd72E2ByhH$P0S&i2 zXOYy>qNWcOmCMyiL~zYl?N=UIcoxkpH3=h^%TF#G3d;Ao=hHRz*GkdNiYLw52U^Su>yDa`$G{`; z@<>S}y&NaNbFzcy+y-ct0bc7Fe(~}O1>Xy)T2CtRzq=Ka{b|Da^Q4=s^cXyv59&#w z+Co)@D;8;&ba{yIexM>R?kvIooatZD$)9TZ(zGD6x$E;?{X?ixf0`!kbJ1CW&03utZ|SB+9tg!(uVvuVtws)wTn_Y=nB7a zA}BE~nq&7MO;|`Ra#@WH8g=GPjRG>2uV=M~9m*Opf&Xc2nrWII%BGsS8z^8aY8!9g zxlx^=c>vTz4PD4Bad|}#z6kd9gA1_EZP?XBIH+j5E{Jo0H8y62+&Cq_-4h}@fSFG_ z{vS|u>&fsbLaD55mQr2v)VGmy4P!@E19e6|oH>uqyxVsBoA_qdxqdx?;q$VN$lXF- zk-xBKujX0QwG;&E1jgW*ekjTjDq{B}h@B1&wtY~!jWm0(+YFiM*~LUeY>!TE(|IiJ zR_X40O}wYMIbf50t{b3f7(SLfY&dzg^jc+Pso|M4&L}8A+GutD+0@RMRgel^x> z!M5DQZb;fEu+DRjq=iK#G`p`J`zvP7lG{q#n%1OwwAb&Gfp6u&@|ZKTIhwa$Jm-Et zXtbs$j4$psk9@XTI>R$(7d|d@!3KXE0@_J9cyacHE*`Saqi!nR#o|>TZrrhqdN{`% zx;hSj(FO>>ZECSkk8T;Cv+WN)kUTz#SV_x3eJ={pj68y6$}{y^-m~TBF||c4UtICE z^}KhDh=svvr;Z3X*(|YKj517TuB2}ow!wRl<72@>zGHM87@?S0h7XBlQy&nCnFSTd&z z4E9;j&kOuo0mIRx5PH;R@>cBkdb%HNrsj4$M;2SJzo$QSa163J%@F@0=$vyDbfg-0 z`?(A)yYP;dzpPRIaG)lgc}~1|hyY5=Xx)P1*}A#XnKi1kSeI;CUNr026802+1>iwB zv4f55w1hR41j_H@dMQ@~%H^ddc{7|KAE%6!&V>9WRb<3i*P&d0nb;ix0WufdUj9c# n`mZ|X|HGr=?pg=iAMgIp67EMC_rJgX{ldS$`S+_@e}DcTr~q9o literal 0 HcmV?d00001 diff --git a/docs/img/mica-mqtt.jpg b/docs/img/mica-mqtt.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4e65df30a14900f8fae4ed1f3496a6a9ff26d78b GIT binary patch literal 25522 zcmeFZ2{_d4`#1a*QMQT5l1!l}p-9R$R7kQVOT=`ivP?+Xw;@RpLMX+EB*awqtl8J> zWHQQ9PjaepW}F*I=%{-EZetlV`b;zU}s}v7vSCr z=<19q{<0dWofx%vfnyk*uH%`I~zMc z2M53GKB0ZG|LuSD3W#eP!vn@fMg|dR3l{?;7X!T-f_n&w# zwByC(uHL@8opl!u5Rugo?e0Xg6;=Dco-5D{Ujzf?&-7mC5DZgoSJ-<)@f$OdfVITiWx{ip4sau@KtS8uno?h-qO+fVo= z+IPwRonUwWmn8dBu)pN$hu9ezz{_Lgf)LO;St8k=_5VKqcMbl3+XG223>Y*tIGXu( zmpn{~wyU~%>x(KW-4;p?k@!F5Vc_tY94{BJ7v9^G?a(o1miy(}(Hh4r|69&Q@4GM^ zW#RJ6u|?Na6R^ns%c5fMKaU0y>k+56A;O@G1@Wj$u_(`1_l-}QhK0OStu}4La474# zj_GsCA&+<$U-C(de(CIg8t#{Z$0jmhQ6#CfCgzAYBZXJuWHSzU?eL$7sCiE^V0mMx z!2|uzKL%wztEF-@MBZ)->qldkVzkLyZrg65@-|$ioX3Zy@5}p5)Jub{V2+RX4CONZ zjk2zb-(x{&aVGnKObBgfq(7=?eBEO=nbCaB?>N4ju!Y0EQ#>h>@&V0U139(phM$RH zteupm|U>virW2zpKr&GY^|5|dn4YEO?5RO&lb!l4D8db z4}+G-%NZ{=&d+5G*D)d@d1rRT#;zTI-``*P_!IZS*|sYZ>2H#@gh>1kK9aEnr*M~6 z)=&A?(<4Wk-QHuK&()E%Hn#XHUYZW+-xcr4E%Eqz!k0sDh+?Z)MFL+9Ra{?+IhxIW zXSj0n*9UN{358i@zLBCm&S71nuodgbTETgtUsV)YRkW_$-O52x>7i)l+v0Zh*wC!R zacAk!HCLyq2ruZydjeZ^{{U~afw!$b<$WHtf4oyPniPGRvMbIs$Kn3Lw3zT)A(tX1 ztq93J;&|HAjP(%mqw)0T_gMP0hzE)-EF>-X@0rBnKk?5*?ls(Cg*IvjaC9g{y6c#i zRAV3=Qc;VbL#ITrGL@~nxT0UbArETNp&HY+RRfmKQ50FiwbSC~4kP;F$U=X-)Oj(tvzY(&hzYqh+3@ja;@ke%mLVHITSxzO0RQbj~g4YL8S( zY3R#XWv#uGGNTPxmREijPKwQfV~r}iU5E}f8C!z!ec1(9kUM+?VvnrYDkR#@uc4wC zeHm8~58&p|sxM;=S>~bj=LEOcm#-&9Pe!6S>5yThMc#5yOJeHFIaSMQqdu!sx8YE{BkHDs`=JdBL|!F$kI3cu($KHV+>v)$?{net6gtfJ6!3IU}1Rn%O`hV zUE6VcTN2B!NFpkm9D8+3K$La`50q=X^X)9E_d*W*QZL2U(niOrIQL+#pGeu>s`HSYF{oCT>emU{KRHb_F6H1>5d6d#nreG7V~ zhc<_bHF$uw`KO1MX+1yxm2dJC|DDd}TSb(aEnbHLdRCKexhm~N&*RXwseu&DX79HH}#zM#yDL zY>s>>S{F}@Ebdy2;wK+UywsW;{;G{R!KkeJ=CS&crp+eri(hX%miBu#ZzVaVANuii zaw~HP=1TtT)C&PKUoRP4Yw0b$-jAunv%=n)&tN9|hay&vF)Xd4d40s~YfQ*G_xJcb zHH&EW=ddZLMRGC*IQeN|pOjOKJ;NfZFgzm?*E+60)c5SOt{bg}cjWQmSRQ`47AmiL z--(t7c`?EHC^84PXqE&I;p(Z|vm`XV+5@3g2 zm0+C4GI=a7Rrazuo%CESa7q+ffp2Qh_HooaeY@AZ@09=e%tJQ`JP<80vDLQsgf01)v#TByYreJ*(CCy&#f=*VkaqC z&>`%0I<#tG+Vu3Z2IT9mp~ZC9R`iLBE5}C2sNX)yDI{mDOUuz1q83~(QKL|$ezEQy zMgOM1lLw}jj0n0~^YyjPYXKdzBDZzbz(JY-R;-66MGC$kwI?{{b%nR4LPLZbqKy?d z89zl^cpa}TUifUexjIJ)K7}g7FIDOyFNR6WpJaZ{c$+KEK58CYI7P6ssS7iM*iezJm_$nz=s|am` z+Gef^_GG5Y=G{+mbtp&*xxmu7XB#$IoaBj9deC@0)G;aieeCItz{~kqJo5ZAfz`~7 zg==4heBM@ebZ+ftB8e^^em6RF0{eoZf!kwUxpHXk_?L&DxjjD5jLdk$tpkm7rMwOBQ)zo>n4{W!_l?%C zSl6x$ zAE{h$vOH5*#HgR^c)IyjG0a$>P~)W54!Jtl^&Vqb!K*15 zTr}+rtVrBt81Yp}rdGLAjD*H%U(B?1OM)-nEw685`Fvb3J0E*|Csn;Q;|(=^D9^7bz%2U2Pc(4DPrlc_oY_V4$&Joz$D7x_vU#R&2&2uHTX&Kq zP4TaihO5Q$t?w0uK2fb}+k{sgV_1a+`gU}mQz2pYk2j@y_}vKqa{YSA>HE=ru#Cw78Y+CSA%xg~aih zb!P2;ymC;ws!N`HBpW52B|;n!Qs~ z`eqItf*-l7Mu$9Hk&{bm!qk@@5juoDn0}Y0)EKQ1y$FHT>vM?Dk|t#a?ZnUpHpB2u zy+T_Xz3~*-DWwQ|vgjwPqxl5K*z3kzR;L4;neTw*_dtI@le-u?+yC@?H10Taq~^x) zaUV9QP8}K?mDr_N_b8A$81~Y?eACx3rIj zSiIlUC8t!zyIL^k#o-5YC-V4>xc5(l$kgH-{wG$8O@Ta_8xetILD2W&fphH{W z3R9@Kn2E&GhdY#`gx`MEMm)@3sTyIGFa3?%yW`lvjS0_;^Y^^PovK4tb>(m}_Ya?v zuQnR4S9~_HPa7PxTZjb6XEWEJTr@NN(A;hQTk31r*L4BPhg0BR+_dI}?Mr*B(A$S< zjdMLx#8Xavz2~RtudHyE(E8BD#1NJ7nIJ=XU#Mzt-O163uko_EPdjQhqG*AN9jmpj zNR%}f@!iDEshoWykGY%gv|BawjShQ4^2d!XI#l7$oN*XtHF(G4(W0??aZtlmHqJIKFdpH3{uepUbmjeTWDd|G-#(cqAWI1hZ)n?PdiQS zZF5uT>5qA&5PRov&x-Vf>x>3;AjNr2_xRj2&GXdK#un5xBY~x+!a4ggdgLIDca^9e zA48pcpGSvy>{JEvcFvjZWAm+#n~Ks;rbAYVs|-0B9`|zy&y5NrqqRQV?~v}$-E4rB zDZgZs z9)TJ9JEjSj1y|ARKF00D0v*&O?Q@AE6q&Odb7KL(LJ~768mAeSk<#)yAk8w-nZNnG zZ*KuE;w4s~x8!6x$#L^*e^}9@#0wgeV!|2Tbm%D3h%x?U=FFPnV&aTbU7aBZ+4RbhS9bf?KY@ zhE^%)bHqu55MB@H3M*v(-{7lcm5u3;$bGPcRy*m?Og=WYH_KON470)GiY4PQbVwu5 zCs(meZYIII#+ihwaEqJI%T6l@lX|=oC&8)zZVoL?9ObI<&6*3?q3!DJdcgP6mqcBA zkLT4g*Co$;*H_sQ-DkY~q649K5-7MLTQKGCN!r zMuK~7l-K(4KCg@)?~iIliMKi&q=~Jnj=!Y9yvR2S^+N>}=TteP7EI;76h6~C*{{d% zd*lusVn~9?k!;3}x0M+3aMx=j5Oy?fA3eGmMiN%N*IVQfd^sP#_Ru-q@^++5qyO?( zF9~w|hAlUz`rdG2zsG!SUCl1?^<0g61{ELlw$;uyQbznI7#D3BpLXT34WZ5AmnU+2 zO=W$%nTVr@BGfCkl*UycIUH6WBzNUKh;=waY1n!D9Xix|ZDIZrV!QI*a11_jxGp*> z-@Ym!oDBddY10(@mp^7OWd6w)wLh=<;4y9v?Cxyi;6gxvz+Nc(;uvwdcWyF-WA4Ni zT)k7!y?w2ovhB5xDnS6h6p}R&xz;xrxwS%NOLUbV3(G9Vd=37v|I}RB_L5sg@vYPY zvMi!b!<6IENJUp32I3@d(CEo^6Ldq+Zd^i@Rq*Qdc zxlSy)D;|pN>G2+Tnw6DocO&Mmh(yCxFV=M@CLj6SHqr3S3m1#J>Y294K1LVM`O+a7 zqtiJ3!26hMABn5TS$8&C_*+DGEjn~=q-hQXxUWB~~eRr5UOT zZzsr9q~qparq8~Qd_8zv@Ms6ie8jrGlU`y>vxjN*PLfUaF7iE%NA_+l{0Kj}^WIpb%8`Ii6v-(`sojQMPW_Fa8|@Y^E?f({Q!wWZ1GdA( zfy)}M4!qTz!U6`0CuSNo)hQX~(nf~jUEYgQZnq+vZT^kbJWA<_9EfhSZ};mRVIR?GA{0=wCgFkl1Fb<8wVEk)wN^*!)R45)NXCh! zITcvpRJNfST*+e|M{Nx{uxXqlUmGX-+ckujTpyAwOzU>XMTEgmclBzQM$`89>ie6~ zq4dZ2Z=NY8%R7~rQ1 zgVV67xz==O53g6oY@nd=)71dPDwoNBiw@nQbl;^Qa@=zHP*moea|JdwkF7-q*>f#% zrsKLt;^oX8jKCkh;jjF>3d6F$@S&spd~5+n zYhThQXWEKTz$!#V!T6cz6i9;?KP>ySzvfYSRq)DA z0!QndWIaU}r=nk3ki2^)AdC)m>%Dk|IsIi*kbF0(I48f{|C75DZ?*W_+exm7fFw>f z9A|3!h{tV@rAx2!3m$7ZiN1JY9n*U21lfjnCWa&L?Jkp9CkOUhm4R$R_PiIp8I1md zqJK&3Fzbh)Coc4q&sU~3BPL<34zklAsCGt&imzy>=XwLcp}6W zPXtE4w(N!RwwD(nr~-1q?jwn(PH!5STu0*a!`|1^#Np(vUeqnW#O2a6`od11E%|Ws z>KIEXX6;GDyva-wERyA1=_|VrbL5)W2{3ZgLz5%EvYrV4Y5W`MuKf#@oS~8o8}Uni zoU@hlH91xVI!d=qBgUNKHXM^|3cmDJrG5EibMyJWyGI3Ok{GwVHPrZJ%zY&V7EK2i zUf>Rdr8vFh$42!Z59>d^*?11~2AiEZK{eRgy`;9_EKa*;%Q&bC)14yiO`|LqM}idfmf{M^VpD88j^#Guv4!wL!tQ<$ie`ONXa)sDu= zp@`dh+Sr>U`IJy3Y;Z2TFlyQI4SF`-88~XYC_#)W2QMB9G63`$XIHMF+Mh1rH`g#J z|3iKbr&QM1Jte;11h)>>rJqQ``RucjNk$jCc8r~UxsTKNzEh0;lm_IX zP%Y(}x5RH*kNwy`RMrYDp2c&K1?iSd}3 zP)uEO`Ge`Xt!^&CsgO^4ntiXJYbmwF?5I`ML}S|AHiThBS2tIe$@HUR=&$fy_Pbb% zuU=xSM9e<-Y1&|nsC*Y;NKAsg1s>D}b3YyG<)NK=+wBe8V6i~=?gzjBzaAoHzoHDS zpx8=GZ8$)}c^P2JktAX!;$S}sFBx{?40h$MC-i5pFdN8mgL!RBI? zp=?J`birO@JqC(C=uvSxWb?P8+73EYNhti*7)|2&y)n9t89`wMZFPTb3w$qDC0fcj zu&xlBicA`SX{{Zn`E#L14e8LBEgcG)1eTZDwsO1#9U{mpP$ZVfPZ7)dbSNi<#^Xdi zkJtcH=xdXOldMb;s|=yQ7X)AK)S}4j#*m>J;7x$Y)=5e0U`XYpDo|p zf(@<4U;aze-&YMZwFA4||Hm?EQ6xHnnMQ{wPw3Fs837oWV#HrlOri0GwNjz0z$J{P zL)Bg7_!NCA3!pcQe#i=XO2UILk+epC5x7t`Vuk@u+X|;0NGun+-m1tNvFQoA1-0-g z^+Kp5Iy6u~hpt~{2~|F-5agPY)^g-2f6tl6(nF2DWl0g0{;E$pWulEb7MxOW()+a4 zT6L>VWV%-R9O&ktLxCoJ1G1OSwQVjSjv&(ELGFJUW532Ceu;82ME?rddB_W-6q<&V z4Xwn64z)WVr8slh$&5JO9d*#T6xFWL0i7G_=()P=- z14oc?qUjrkS%M=1x@rsaTK^=m!_F(O!mxDuY?Jq)=m?5eNuU8RCu4f4ZfNSZ-aHDg zS{YZV&+XoYjcwyeHm?9RvF&G0S8`&(f_7SAgh&9Gi<%eQPyz(iP5aOOXOD!Kq^^*U2| zdY!C{2>CP839tHXo*nRXrp++Yp&Lms!6oDc4-0)-BR>DxH6!bqo6qYuuU#*{fSPiU z?!&r^tDY9K`$a`en;CcY=S)gPN}R?WI`S4H zZ@^lFZlCjebP+~nYGZklp&b4uj|=-q?4~VN-1(?- z{+YHnEDDiwj6=o&PHlqZke^t%zcON!O~+RstUqz6) z$?^-fjOHa#E916z7j1uQJM$*o_}bT^)g5$OVL53x9r^;`kL|M)c~>tTVnlCR zD$#gQiDi51IzagYK)y?0k3KBN^8su~Vg`7-@|+g*5`f(ZHTyBe1r&gTUI7eouwX&E z&@z2>HHO&)PSb+R63HqGoX#9N z6bJ#W6ga&$__|1 zXYxH?%No!gb$;hwV^jW#Wt&gwExx+lNkF}v!b-(ThRGRvV2%zBoVz$qveYMqK;+LQ zDdQ$_ADeW$K_snz`1DuJNt*6P@2i>q5c_`l#FxUy2Ty&t;wE+*!B}&{WBMHJ3*Ir+ zKJyuqrWw^;y!j}x79j{NF9mu5^{R0vP3bq8eT@;OdRjC^^U}sLGyLLf7bh+=dnVkK zXO{>=cUE9c@e&zd5pXZtA`Wu_HGN3@)!hEkI z56q1$^Ou1cK18gFstFh4Pt(*D3#dNgv_Pv&E8|~^KMXrh_u7#SMmsbSKSJj43j3u9 zc`POXYC!3CYQXgeQ}9RehjKD~{e|e?9Ge>_)=wVt5R5Gvc)75vU!ODwa zU9qgBRyrgalSU2fq(hq8zyM(HQmEG5EBrha@k9?bL*8h@MNpntiq0$09(s|F|05!v zz=X`f{AIS&1Vj)d5(r>=EtZ>;;bZUVP^hmjje;Z}MNJ2E`vI-l^o=UHp7sOzOu(2E zyHRxLve5=>&abxfAEA{$eipWd2&Sz$-E}my;I@2MfP6VwuBJ6^nc$ID>udVC`>ji^ zQ|z#uWB_$)mdvox#&U2}N^%of8}Gti6ehOvYPBD89M%i@$*<;lvV5`>mw7X{Snk4R z+uF-M(tuVZlA-0+Fu{VD*JgHSM~-y}yntzp`n{qVw@~06y3--oy7>JT8%2Q8<4|Tu zcBEt~jk%>!Q^&pYOq$)2EU@{i>ojHaQc)ad+?vdGm2qj|<*OCsWJ(Hg2+SqvcVmM= z#}r5}zfSdT07g*!#*Ht)KNzOSSX=(+)6uC6pUod_z zdgLO*ozb!>7{r?pgke5swJ{C~!%aqTL z0Sd5nEw^F%uJ^f*O`a^tykpW- zRrd?KFKbmYbE32o(NjAZ!gal7;uvDP^>404;`S|F~ku zZP+OZhJQ+17^O)!i{KAGILmDDpbcgeg--Q==2Y{(mhX0YZ1>Ul{!+q}UE}7Vg;ts} zsy2A|o5j_n|C5#VH>~*+6aS8pJdZx{F9Vi#G|Z*^D`u-5fwwwZ93x)jr-^2VRTjwW zbalEEXU?ilmgpW_ulOwaJYq~g@M@T${Pl|lTLPS9Aan)lDFNVz)J`LOR&Bzo;bSX< zvYA)KEi*Xv53{`frp{$o>;(gEG)E6=N6$6u`s%#;av<(iznNB8KH;$Eo_Zln^$Jpf zblg|5v&KZh{4Gs4wRj^nU+#Kb3_rA9R z0K3fYyHjjKQlbR{%bwxqIyBC|>VzP0-|N6Pq!j27m)ag?a02(LoTin&0=?_{?zqv;5(IzPmM{;dgICGHh1;V)yNM?l(cyF6 z6!lr-sdcvMyk9UP$P-ajYPu;|8_O-{5N^5*S2;4sPCJ1J@)`m{%WNI_GI*&alaYsz z>qLu?IduemzsxVJnQzP4!O7i+s=|Y_zO9|JBDx07)|58T;GXVjDkb>L`p8+gvXcUA z;YFG4J{tHJb!=IuV4G*>DK(HQkBM(=ajN|pHmP$RxCnfm?0ZU1eB1Ao+dpTbvSWAv zTBT&6d+z~LOe%9SgB(|bSeBSa5^)GxsQm`2Fv*rIgL9`uXGziq&@B}#XMN((-^{0S@>kF)7uJy^12lz0FBM%*3k@#;%n0q2=^q$Agtw-h8NhzW z@GXkWUh?3i|H96d>2{Y-9Q6el&5fSXEc<|sc6HidN1}VHF=VbJm?dc+*rf>;w6dLG z`(TZ+hyM1#5BYxy1$~bUIsF+Z@>h2MX6~eZiwsRk&MR&xd@KGLD7yGPGGwYWo~5*D z^vOJ{P{5fX2AY4eQHxl5;V0jILH7QIw+u{I0*C!oHF;Ovs~pbT`!T=UpR@nn@y{y9 zsqJ*gX93GX&$a>uZW3 zaWJZU_`5Ru1K2O?*XSxO(6)Yb74X*534))_r9INeS>~yGqj-s7HLu2x>hC@?-fadL zCy+HIschQm$I`8tGdd(YlRwKOauDV_yNqcSprED}^eQc7E`EIX!f$QZB&-zg98ZMZ zCC}AKO$Et~HzREQg(uigQ0`(X+T^~xNe=njd%l?GtG5S!_G6bLY%yzGS__**jt(HT zu>|>;b>B}J*DBCtR+5MnW+C`a(L5-U_ZJUY6sJ33(~1)cbd;liSo-O0;bu~NFV{X~iE*!P3 z-e^@kWY&+@!(^aZFt28ka#crXZ(5Uz*qs-QbIjXCXRXK?Zvg3^bOkyBCBdGfaaJB! zntvw^MFR|82?0rE03Kpl90fJ2D8V@O3BOc>S!Kr1$VdVJ+#vU{axx=ObiOjA-j0g~ zEG-y3RvCEJYT#J{6$ITWlC8VYZGz~{S#&K(Sn*eN1kF4_vn{RPn}Kb_x>AD#eXbx0 z*jKi{&mr@tIhcYuPV6*pfLLWQ!G_9k1IQ5#dJzP@ERjt8Ko3cWN7nn%VQ_pl<@=<|mjH(>NY)H-|2~k_ zw<^besbW{Y1k{Eg`CN!JOG`Ou2W;v;g++kH%K;OoLnAsg`PCR6|BV6~???#HEF1{x zYWcV}5O%2sy}I@L0%Bi*aLIE7&XV+3BlclN!kI%lwsAJp>LE(zZ2ULUF6%fQ(tFHS ztQ2a~Dtz;1FycYh(C)fnv3Ax6@&X6$p__JNhNV?be#;x}xLqSecsi-!;>h~09uef+ zKT=7DIPGzXlGeZ7^3p~vXZv<8(#(?BmpFXPJMuJje2tkgw>>y5jvfVA1sks`^%7L9 z4|sC_g6y`FOs1c$4w5|Il5AWiHHJW*P}~Fc4fL>8CON_@`qRh`S2+8**yUXxJGD2G ztQ);wF09Uggl4jI$jeu#UFM4kd)6>VVp*1k^lxchJw*LSrs+?&T>J;O`44XSseqK3 z@?W9(-&xz)e_Gq+qjkVB|6y%2xsCxlV3`}Z?Ac%oz7$+r3WP63yL~WKuMsK;(%rJ_$cN>X6s;C)ftl+z1hm0Zw<8r|B=nAGY%3$Zo_0 z2RITS18PQoPX3qjF^2Jv?v#5AzzRXnoGQy%KpwCgS>^$oF}s0M<7@M$Q!Q*rh_1Tt zeO6iB@tMa_j?XOkoy!HIdWg}}9VH=^WM4%Z+kj1^l=VesO_?Cn8aZtigWZQ@|xo?figVyX{)tO`oX;kO zd?yj7$ya)^e5OZXg4#G;gSuOnRtN4f&773*J|Ub^{)Ws-78xFs%=B2?8!2{QgFQTW zU%-rd()OPjVlcB_9a2+DtG85H3QFq5)l=^W53X>h6oxF!L*WDZFwzaP6d$YM z^!qztd*LM`{)lJ1EAK8WWrPH0$ zh7UB1`o2|ypHg`+;cLq{OXV}o?{x`v&}`4)nJpHW)OS@Ko$%v*iBtw@AXOv_gX6t7 zTT(A8EM2^P)$w@+&AM|IsXj2M;p!xWKO0pea$oE8Q!O*2ZF+lgP7|<9A9723iEkE? zhg2p9%RR$&j-EWMKc$_-8C>NjWPxQN1z#?FAUjYgQXpnCU4K}skROg0z5IA0gbcsb zwy-J_6i2o!ILMLWc=Pr`_fx7KYC2}Ki-8yp_@cc{`s6W8y>Wbc>WkUqHa$!LZ8a?Z zS&5Y(#;xHb?PJ8RQgU=CBF{U>!@U-}A@CMPJsCmec*FRTB$7hoTF@Xge1vGMh}zOq zC3T|_lu&RYiR*x<3|XD~S1IE^Z?JE_YZ}ZM`ybNWro!O~k0|5c8*XV)g&%#C+hcoT zT>`O&Z0aa=3xPH;mBNQh@UPz-co3KOI6#>RKTqBE6*V)YV3^mPOLBTU!EMo892vZ< zI-cqYt3hnddK7HcnLOYmiGEd^a_>%i@OuQdC&T9u;kQnCoI z8T22}`NI^I{KFJ=A{_;$sOxuAG+>WM*ao~`0r@+dFn~v5n1_C#Tj7krOxNE+>a0IP zHZ^~QY{<1`dfoaI-K9V2gujyq-{=Gc;UEa=d_>P2C^h-P%Ke&aOcY!m2@9r)k$;d2 zYQl)0*n}9L=V?OrZWv-8;wkp+Us)g7?zO@@a|t&dJKU&t=bFr#W_&t84je@LXNQ27 zciN=G$wl{^1smzSDOaNUOx1gqLe;!BlF}G+3Qn7eouendPRg<6Zel9TdZCS$w?I3D z4^J2?MbwvAMj!Bbeh;o}Lyj;6N&&l-fVe9k!FEn}P}Sk(37t0q625ZO&aY_pHkNW* zVaaxij-yzoWBZ7JhtNW#Y9>`nm{@=msa@g;6?3egyZtte32tq9BaR?bqiEGlYumgG z*!07Tz0IxNg-$*Gs^Q5cr)0v+8w-|l2Uy@=osiV0T^D?8d+UGOb3eXl_Q{U^V9P$8OJcR0+@kv*nk;Kt(+> zj`H+3=+F^$Yq%l45f8QZ<^iR}HTS`uZ`m=j&=^@PxMsX3$Q{Av!$S^R^&(QPT^h*0 zVgjGPwuk0VD?e@IA&JwiX?{OK3X$U|+=yy>p#(frYWqU(tW_)f(NC-z+=rc$Sl^^D zsLeo0Ea87_Xx?gyI{uj7!A@GIeMbCSw$0bdo|)82bmUjRyaAABBzoDR1?4Qp$E9qf zSz{@5C4J9>?3_=S9WoSg@xCGRd0p#R{{ji`Cn^yIrx$uky=(m#Iw_Z636T!z5|x)T zrf<>a!^es6R1eb!%2JsgH|FcRvO*+^w2i!a)1PRY~QUUA#Zjd+6; zfs`36D>6)2gz@>FBN#&=Kw;Zt^rHEdjhqF}#@2SxAwLOiQ5yNix4fpo4|VLiV%h0} zk8+MyFb?*{dlZj#huzh-N#ZFfo^e#Hffv#?sH&xs^+f$0D8X?Xqb^5j&D&ff6X5nA zBc3N-H7Fg`d$(6NH_8+7AYeI(Xx#?Of{YWrv-CEm6SbwK(|mG{4t}mJ?Zp)Yl;if~ z(Av91;r;Gx{%JHb@pZqn^2zyUB|GuMtyLB>PM4(fly8wXL_k3P2H z^OZVh$c-`LiAl?U1e_pYls{&a4h2g~?@!du+p=8bR-M7D-MWq{Pxe2eEQ8}vsx-KK zI!&eO@ZuiIH2ZDl6BWGWzh5RW;d@vjo<_2R42eZ0*wy44vd^R3cd(|X;bk9V#+iXM!pakp9K{R+mq2M@FoV+lCiW$>^TzwyhJ4ruiTX6$bn@o9ZH9$|T5updJEXt%+0_ z8_Gd~&paH%F*KZ;52Ph+4{O=8wCVj0Yat1dX9T&RwK(++rCWx&Qnv`5}VX_hWNyDGhqz<(A-Z zeY-Vpi4Hg4Nihk?W+deOpLauk-5#dF6nk(2qpE17h&4CtEi8-#j;g3sFx3l$tZV%h zW$q%E4WhEvIV>q8PkAGlT|)<@x${zp}}eNNNrmReSLH8IwI#Siqw8=hLHPKgs%0z^&sRMwzx!!!m}}eu_l|d#5YNf4ZxwCY!t-X#YXe`; z_c%Gc>93Q0S8-MK+PYDf;6_e${IRk<1m2$b&t`!07hN4pi$O$!jCm>EdXLk!ihij$ zZ-FJE(ut*0H=kWgFfKSKDz@O#Xw zKw_K`#g;Y2+o8}Vao zIEnlJld*whBYz2oC|Gs+YvlGzBLn1b1EaqUoPw$D-&3M57GLUb%!FBe(z=qw>0URi z5?a|a*3o{!@`7<=`s7vQlENx{f~5H}-QIj*3FCW5zU%8P1)=dToK;^g#%I60gPo5a zZi}Ih6xe+@uzUOL>g?(~@{Ho)2Zmj;k$1P%%Y#S(bV-V*D3&Fu8+t?XYi&%tfi?RHue&u0UcXFul`zWov zTW{jF=?J+nD#t_kC1!N}G{<<>rYn_XC z^m#uz#h(dNbaaFKavApX8oNsJUCH!xY}SLg)5U16_CE}fL))=ZSx*nI@2J+_s}$5a zt<6EqY7W|4bl;S5{{^)=5ScOi9*=)xZ$A7irTSWzQ&RqAoAq!8gnAc^7wloSot572 zsRd3O3gB<3=xf}tOB87*Vm4UpkN2{i=#SzUS#LG(1pfOW>{`P8vzre2T2y%?A&Kpx zF>*l(wm}8hTb@ZfhUt$-Q|OQddZZg17pOqvBCQh~9?ZPcCb@iEz55uUhdcn3q712R z>&HVSachxBE=eb8ir(Vb%M@Ymw3ZM|0g;s-jq^a`E})L4e{a!FO;4jkt;7N<_1nlU z#`-^vEUzqAbL%-SQtbTr?IWVAzp_bxcl`ft83gDAeGciK8`IS~Wjk8rlN;Mod6H;E z5g$nFD8U%F$vbdV+Pm!>xHc+>ZLI722(Rev<_#N5V3pw_`AzFZ+qzq}pQN$CUFeYh z%VP`^<;*rA2wuzT36|Kld+Yn2L~ekTw4&Z7u7-3E3nb^u1?<-eaKn_U4u2{?AFoxb zBI12bFg16rcdyUXewcu2%8Q8Ch@EP}aYz-4N&LwYcNfvU8_t?x5tc=?yuSHz{@m4E zk5voL)FM%~!*jfRBLWR%XDidRFZQYuy|LR0blqL|*|@ErydmCM3K@hnPT|}&oDPT1 zy*ZoC841jCRR#F^YOVEZ7xjqTnyaObw^==L*~>3yS1Ct3)AOoDNwd!5a?hrEWNZYu`WB_WezRk8uLnBp~x<1OE|hHYpkp z`ls61w-U*tKA4{ejrh4TFENPcGAx=7y~lzb_0Llb4Om969Zy0`{_%Ch8+2<>>$l1L z(S5WZVzP0ZMpYnPNH|1QKk#{!VWbIxLz=dqg+;ISMU5Ee#_Jae)i12FZ^vdv_MG8- zCHt;^?*tc8nWBMzTehcnvM@xBigBAddzq6b@f`fT+ZqLB`e}q$to?9GXCBH^+TrT% ztFa==)yTzByjGehyqm!C@kLZAd8v_ES>=5-wW z5YI({^^+$kVV+tKOLWm=rA7;;X2yp3{6>qlWr1Dfc&IvL7!};dt}n{)LL|!B1n~je zN+}#fkGux<>blc?Ri5ao{@sJa2L|Q`(rPk;sM$)599wZn%$_lAeA#(Haa zYgA6g%R*CSvH#+)H2ha5P4XV;<5|2|;gU0aC3~?D5jwIg0^%G<;19~F2{ZC^SPW_% zbI3n)rO|iWNo?jZUriMY&73Zg+NK94=Q*pxwMIR#jBU9Oh)%)6dtKBV-@Xo?v=CZ= zror`~VN{zc$2=T`pR9c*di`tap#Pc{`1}<3QxDK;hQwPew|&YP`~ofcEC*WN`q zt#3CN;MHN)o7T0xpC7RwueQ)sZ)F4fQ&`H9@6$fajyao-E*QnbR1P=7nVpl0B4_ov z=}2Ckxve>?zZYZ0K&b&o9GJ#ex)JkV02%MEQux9&*5?c<`oJ^g_~XHQ`A{`A^#aQg z>!wt!(ZGCXF~Q`m3xaJS-f88(V`cd3cjcim$#cS~hHSO4xVk-^{|8@tUD{#$_^e20qa zwhj{svkp6z?9Y4g3Z%s93VAS>;<1<842Hj)FKE|TarEbjIZClWS43M_L8OG+1%`I2 zdoe8`r?_|5A!LO=wK~&!4Mzt0P@l>qNmo^NTSQ#QeY8JoP3KVfF7@?t<>{3qm`5w` zm}xn4UQ_7&!|}4tyV{b9_ucE6qN)9>@L<7lR3b-Sl+B3qP2G}L#Qk3I%?a$hr32() zK$0BeTqoO5DPVU!$QInxDtzC&zeCKnf9xdXZlhjDU)^OJc#y9p^0>!i@RyBRK+xqK zhSsXKI}>kI%`Z^i$*|^$Acfo_Nn+Q#Tt+?tTo^+QNhHB!2z6BsM%5o%N{7O_Ul?lG zKIW1;B9?BQ$U~^lM$ve%571yEoCoL;g(mPW0>83}b`YPsbdR|0?b7Rln90xQ>!LGo-W05$S9yYlrx3`N5Wb2oQ8MGhD0D?4TOD*m|JjZh^$v&csQ1H5g& z->dnLe4hWx9Qdz47b*``=13)F@9hlSI>x@g*?Ouj1vbVzX&zh&=QeLPX|u4>=!-H4 zCU89RyHUCp0^xn-a4Zj`uN(TTtvTCF?WfdIwh+i6L<~FW^m%+%dUyld`SJCfhNf~7 z%zosrO!8mua{b-e2jxTd<_?Ki>-{mS+f_;e*&iVcXME=y^2!5uz6v`K>Jz1@cSZp& z3A<7iND8oO0OyXkyi$f+N8*x6|JKDNpt&2cr}1f3d*rHC_D^;M8>ZmNrrB~0v855kwFVskGf`@owQvt_s}y+K+{A3HHClg^Z&GS)a;MEn>Vokv5jZNL}GNs`7%PEuYs12b}md{;wQ ziwFR=hu6K`8J;5SFhvT+qzHe2)4@KPzI0ZC)G%&#Fc0WJs!dL3nYw#%}+JzE?e-obfygQ@NdLv^WzAnwB4 zRlgDqrp@q~G;$c}f*I+_Y)}SCP5(sedn+k-hGzbOq$4vm1IEw>$rZ4M5?V}P&XMIu z)(mgC5_9y00zdIl=4jrgY5fZ1`ei-|)vvHeP1;_m4W93FF)MSTUhtbEvbb@8z;vNh z4T=|n7V=_v7ekUNFbG`EDBF#rr^Jo;3+QTc$R;xhH><>8w)MTk$Mux*&F7k5BBSTW znRgbRtsUKE7r;I()gw*3EG{D;*IF#DBLmiT=&g*h+Kzrt-m4 z6ZD$Vvb_zl&5y09Di&f2Do#`8**t{fGK+a`gD$oqo?|9Rd2}75cTZ%KAk9O8Om=@0 zEMBGStF7|U#e3q&hO5&Jk2MfkDHxQYotAQ7lLRKXm8{T&?K{WlLUr$UuI?coR`ign zCJC)uR~O|4((5+ZM+%NsLd}_YPjR&sv5a_X6eUi zSFv|{*%izssQ7l?_PtrX3HtmzWY!Eo;|+VRX&=&peN+uk?nN?OqD%GKN&!cqb$f(@KNdYh$558)uRI6L`d#NZK=78kENx_pT;jA}$sy0*eFiu| zFNw^d-?Fmfs0^&8Tn{^p86ZH~QU(Pw(u>}gv^IbxNSc5;Je>psDOIXkH4{9-{L;-MvUpt4Gy|>5d2amO6#UmMZTs_1?WcfuGy>AG;QGy0ZU$esf7ZV_3`~$c$9FKe&Cb zOKObN#_CJL8CV~ljcQ4=T*fQj;0jk|pjQA|JVQ;swGY^sIR8YAYkHJqF6{)oiFY8k zFfK>JC@aUsQmdYO+WK<(8EdmkUEitDwYLjjSDnfTUE~zc()LNs*TMBiH$h5uJDlZMm;Q3BE#R+sV$P!Ar+B}JRJ@qQ!%Q1# z1>uYKsnfsjJCgBP$g?qIsPl#bQq3D)?nM#3e1p5kz0<|UzFWeUgeAbe8Jy78-U_oC zNf>W9Z!Nl_9F6=O76kBRXm}B^KN2i+6~2(3vr-C;*&0UByo#0QDGH5aG%BQPC_W3w zMBBo)>n}ce$(YcKaLh}u|F))*64hI&+?JbIK((Q8W8*&#KRoIi5zPaPG&-c0pM9pF zWGyk9qZ8Q>X?y-tqVX!FrR4m1CcCJFF!Af1L(wO>^YurA?i)YXw{fm*pGRw@(#Q9) z_V6^9t&1DS57y|o`h&eUC`CmN1y_%;E3xvINZvua?!4^_`n6e^YB(a!;6R#^%}2Hx4Bb`fM95bx z6YOq3I2!EZ*48-3F=}qS&R-Jv*$?G;%!M6J&Yd+cIx5HyMeVvulr!1M!jP!vsh_ni zj~Z&FU$$#fUf^R-Cl3VXSX31|IkY>cdaUJNt>Mhd@+#A8#2EX$gSLE?gdD3Osf+du zNCTX1>{c?43SalfZdW6R*61%CxMyn*4VzxYHun@|!oDM8eAh+24A%9+gF8E1_*Y^@ z56m9ld)r| { + 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/example/README.md b/example/README.md new file mode 100644 index 0000000..26cb361 --- /dev/null +++ b/example/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/example/mica-mqtt-client-solon-plugin-example/pom.xml b/example/mica-mqtt-client-solon-plugin-example/pom.xml new file mode 100644 index 0000000..878b7b4 --- /dev/null +++ b/example/mica-mqtt-client-solon-plugin-example/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + example + ${revision} + + mica-mqtt-client-solon-plugin-example + + + + org.noear + solon-web + + + org.dromara.mica-mqtt + mica-mqtt-client-solon-plugin + + + org.noear + solon-logging-simple + + + org.projectlombok + lombok + provided + + + + diff --git a/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientApplication.java b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientApplication.java new file mode 100644 index 0000000..cefdd5e --- /dev/null +++ b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/MqttClientApplication.java @@ -0,0 +1,19 @@ +package org.dromara.mica.mqtt.client.solon; + +import org.noear.solon.Solon; +import org.noear.solon.annotation.Configuration; + +/** + * @author wsq + */ +@Configuration +public class MqttClientApplication { + + /** + * 先启动 mica-mqtt-server-spring-boot-example 再启动本项目,进行测试 + */ + public static void main(String[] args) { + Solon.start(MqttClientApplication.class, args); + } + +} diff --git a/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/controller/ClientController.java b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/controller/ClientController.java new file mode 100644 index 0000000..cd88ec5 --- /dev/null +++ b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/controller/ClientController.java @@ -0,0 +1,26 @@ +package org.dromara.mica.mqtt.client.solon.controller; + + +import org.dromara.mica.mqtt.client.solon.service.ClientService; +import org.noear.solon.annotation.*; + +@Mapping("/mqtt/client") +@Controller +public class ClientController { + + @Inject + private ClientService service; + + @Post + @Mapping("/publish") + public boolean publish(String body) { + return service.publish(body); + } + + @Get + @Mapping("/sub") + public boolean sub() { + return service.sub(); + } + +} diff --git a/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientConnectedListener.java b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientConnectedListener.java new file mode 100644 index 0000000..65464f8 --- /dev/null +++ b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientConnectedListener.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.client.solon.listener; + +import lombok.extern.slf4j.Slf4j; +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; + +/** + * 客户端连接状态监听 + * + * @author L.cm + */ +@Slf4j +@Component +public class MqttClientConnectedListener implements EventListener { + @Inject + private MqttClientCreator mqttClientCreator; + + @Override + public void onEvent(MqttConnectedEvent mqttConnectedEvent) throws Throwable { + log.info("MqttConnectedEvent:{}", mqttConnectedEvent); + } +} diff --git a/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientDisconnectListener.java b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientDisconnectListener.java new file mode 100644 index 0000000..8e8d7c7 --- /dev/null +++ b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientDisconnectListener.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.client.solon.listener; + +import lombok.extern.slf4j.Slf4j; +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; + +/** + * 客户端连接状态监听 + * + * @author L.cm + */ +@Slf4j +@Component +public class MqttClientDisconnectListener implements EventListener { + @Inject + private MqttClientCreator mqttClientCreator; + + @Override + public void onEvent(MqttDisconnectEvent mqttDisconnectEvent) throws Throwable { + log.info("MqttDisconnectEvent:{}", mqttDisconnectEvent); + // 在断线时更新 clientId、username、password,只能改这 3 个,不可调用其他方法。 +// mqttClientCreator.clientId("newClient" + System.currentTimeMillis()) +// .username("newUserName") +// .password("newPassword"); + } +} diff --git a/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientMessageListener.java b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientMessageListener.java new file mode 100644 index 0000000..d82e63f --- /dev/null +++ b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientMessageListener.java @@ -0,0 +1,25 @@ +package org.dromara.mica.mqtt.client.solon.listener; + +import lombok.extern.slf4j.Slf4j; +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.tio.core.ChannelContext; + +import java.nio.charset.StandardCharsets; + +/** + * 客户端消息监听的另一种方式 + * + * @author L.cm + */ +@Slf4j +@MqttClientSubscribe("${topic1}") +public class MqttClientMessageListener implements IMqttClientMessageListener { + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + log.info("MqttClientMessageListener,topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } +} + diff --git a/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientSubscribeListener.java b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientSubscribeListener.java new file mode 100644 index 0000000..759426a --- /dev/null +++ b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/listener/MqttClientSubscribeListener.java @@ -0,0 +1,68 @@ +package org.dromara.mica.mqtt.client.solon.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.annotation.MqttClientSubscribe; +import org.dromara.mica.mqtt.client.solon.pojo.User; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.deserialize.MqttJsonDeserializer; +import org.noear.solon.annotation.Component; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * 客户端消息监听 + * + * @author L.cm + */ +@Slf4j +@Component +public class MqttClientSubscribeListener { + + @MqttClientSubscribe("/test/#") + public void subQos0(String topic, byte[] payload) { + log.info("MqttClientSubscribeListener.subQos0,topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + + @MqttClientSubscribe(value = "/qos1/#", qos = MqttQoS.QOS1) + public void subQos1(String topic, byte[] payload) { + log.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, Map data) { + // solon 插件为 2.4.6 开始支持,支持 2 到 3 个参数,字段类型映射规则(顺序)如下 + // String 字符串会默认映射到 topic, + // Map topicVars 会默认映射到 topic 中的变量解析(v2.5.4支持),注意:别跟消息序列化的冲突,消息反序列化不要用 Map + // MqttPublishMessage 会默认映射到 原始的消息,可以拿到 mqtt5 的 props 参数 + // byte[] 会映射到 mqtt 消息内容 payload + // ByteBuffer 会映射到 mqtt 消息内容 payload + // 其他类型会走序列化,确保消息能够序列化,默认为 json 序列化 + log.info("topic:{} json data:{}", topic, data); + } + + @MqttClientSubscribe(value = "/test/object") + public void testJson(String topic, MqttPublishMessage message, User data) { + log.info("topic:{} json data:{}", topic, data); + } + + /** + * 订阅,参数为可选,但是参数数量必须大于 2, + * + * @param topic topic 参数,可选参数 + * @param topicVars 订阅 topic 模板 ${productKey} 中的变量解析(v2.5.4支持),可选参数,注意:类型必须为 Map + * @param payload 消息内容 + */ + @MqttClientSubscribe("/sys/${productKey}/${deviceName}/thing/sub/register") + public void thingSubRegister(String topic, Map topicVars, byte[] payload) { + // 1.3.8 开始支持,@MqttClientSubscribe 注解支持 ${} 变量替换,会默认替换成 + + // 注意:mica-mqtt 会先从 Spring boot 配置中替换参数 ${},如果存在配置会优先被替换。 + log.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + +} + diff --git a/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/pojo/User.java b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/pojo/User.java new file mode 100644 index 0000000..03459db --- /dev/null +++ b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/pojo/User.java @@ -0,0 +1,9 @@ +package org.dromara.mica.mqtt.client.solon.pojo; + +import lombok.Data; + +@Data +public class User { + private String name; + private T girlfriend; +} diff --git a/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/service/ClientService.java b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/service/ClientService.java new file mode 100644 index 0000000..1b85f4b --- /dev/null +++ b/example/mica-mqtt-client-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/client/solon/service/ClientService.java @@ -0,0 +1,32 @@ +package org.dromara.mica.mqtt.client.solon.service; + + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.client.solon.MqttClientTemplate; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; + +import java.nio.charset.StandardCharsets; + +/** + * @author wsq + */ +@Slf4j +@Component +public class ClientService { + @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) -> { + log.info("{}\t{}", topic, new String(payload, StandardCharsets.UTF_8)); + }); + return true; + } + +} diff --git a/example/mica-mqtt-client-solon-plugin-example/src/main/resources/app.yml b/example/mica-mqtt-client-solon-plugin-example/src/main/resources/app.yml new file mode 100644 index 0000000..cf796e5 --- /dev/null +++ b/example/mica-mqtt-client-solon-plugin-example/src/main/resources/app.yml @@ -0,0 +1,36 @@ +server: + port: 30303 +# solon 配置 +solon: + logging: + appender: + console: + level: INFO +# mqtt-client 配置 +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: 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 时间,单位:秒 + 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 + 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: /test/# diff --git a/example/mica-mqtt-client-spring-boot-example/README.md b/example/mica-mqtt-client-spring-boot-example/README.md new file mode 100644 index 0000000..698eaf3 --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/README.md @@ -0,0 +1,6 @@ +## SpringBoot + mica-mqtt-client 应用演示 + +## 启动步骤 +1. 先启动 mica-mqtt-server-spring-boot-example + +2. 再启动 mica-mqtt-client-spring-boot-example diff --git a/example/mica-mqtt-client-spring-boot-example/pom.xml b/example/mica-mqtt-client-spring-boot-example/pom.xml new file mode 100644 index 0000000..aaccd1c --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + mica-mqtt-client-spring-boot-example + ${project.artifactId} + https://www.dreamlu.net + + + org.dromara.mica-mqtt + example + ${revision} + + + + + org.dromara.mica-mqtt + mica-mqtt-client-spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + net.dreamlu + mica-lite + + + net.dreamlu + mica-logging + + + net.dreamlu + mica-openapi + + + org.projectlombok + lombok + provided + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + + diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientApplication.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientApplication.java new file mode 100644 index 0000000..eb9703d --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientApplication.java @@ -0,0 +1,21 @@ +package org.dromara.mica.mqtt.client; + +import org.dromara.mica.mqtt.spring.client.annotation.EnableMqttClients; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author wsq + */ +@SpringBootApplication +@EnableMqttClients +public class MqttClientApplication { + + /** + * 先启动 mica-mqtt-server-spring-boot-example 再启动本项目,进行测试 + */ + public static void main(String[] args) { + SpringApplication.run(MqttClientApplication.class, args); + } + +} diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/config/OtherMqttClientConfiguration.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/config/OtherMqttClientConfiguration.java new file mode 100644 index 0000000..17f517f --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/config/OtherMqttClientConfiguration.java @@ -0,0 +1,31 @@ +package org.dromara.mica.mqtt.client.config; + +import org.dromara.mica.mqtt.core.client.DefaultMqttClientSession; +import org.dromara.mica.mqtt.core.client.MqttClientCreator; +import org.dromara.mica.mqtt.spring.client.MqttClientTemplate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 示例多个 mqtt client + * + * @author L.cm + */ +@Configuration +public class OtherMqttClientConfiguration { + + @Bean("mqttClientTemplate1") + public MqttClientTemplate mqttClientTemplate1() { + // 基于 clientCreator 的配置构建一个新的 + MqttClientCreator mqttClientCreator1 = new MqttClientCreator() + // 修改不同的配置 +// .ip("mqtt.dreamlu.net") + .port(1884) + .username("mica") + .password("mica") + // 避免 client session 冲突 + .clientSession(new DefaultMqttClientSession()); + return new MqttClientTemplate(mqttClientCreator1); + } + +} diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/controller/ClientController.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/controller/ClientController.java new file mode 100644 index 0000000..64e408e --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/controller/ClientController.java @@ -0,0 +1,32 @@ +package org.dromara.mica.mqtt.client.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.dromara.mica.mqtt.client.service.ClientService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Mqtt::客户端") +@RequestMapping("/mqtt/client") +@RestController +public class ClientController { + + @Autowired + private ClientService service; + + @Operation(summary = "publish") + @PostMapping("/publish") + public boolean publish(@RequestBody String body) { + service.publish(body); + service.publishHelloInterfaceA(body); + service.publishHelloInterfaceB(body); + return true; + } + + @Operation(summary = "sub") + @GetMapping("/sub") + public boolean sub() { + return service.sub(); + } + +} diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientConnectListener.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientConnectListener.java new file mode 100644 index 0000000..7ecb169 --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientConnectListener.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.client.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.client.MqttClientCreator; +import org.dromara.mica.mqtt.spring.client.event.MqttConnectedEvent; +import org.dromara.mica.mqtt.spring.client.event.MqttDisconnectEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +/** + * 客户端连接状态监听 + * + * @author L.cm + */ +@Slf4j +@Service +public class MqttClientConnectListener { + + @Autowired + private MqttClientCreator mqttClientCreator; + + @EventListener + public void onConnected(MqttConnectedEvent event) { + log.info("MqttConnectedEvent:{}", event); + } + + @EventListener + public void onDisconnect(MqttDisconnectEvent event) { + log.info("MqttDisconnectEvent:{}", event); + // 在断线时更新 clientId、username、password,只能改这 3 个,不可调用其他方法。 +// mqttClientCreator.clientId("newClient" + System.currentTimeMillis()) +// .username("newUserName") +// .password("newPassword"); + } + +} diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientMessageListener.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientMessageListener.java new file mode 100644 index 0000000..9b0b07b --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientMessageListener.java @@ -0,0 +1,27 @@ +package org.dromara.mica.mqtt.client.listener; + +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.annotation.MqttClientSubscribe; +import org.springframework.stereotype.Service; +import org.tio.core.ChannelContext; + +import java.nio.charset.StandardCharsets; + +/** + * 客户端消息监听的另一种方式 + * + * @author L.cm + */ +@Slf4j +@Service +@MqttClientSubscribe("${topic1}") +public class MqttClientMessageListener implements IMqttClientMessageListener { + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + log.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } +} + diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientSubscribeListener.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientSubscribeListener.java new file mode 100644 index 0000000..17edc6f --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/MqttClientSubscribeListener.java @@ -0,0 +1,70 @@ +package org.dromara.mica.mqtt.client.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.client.pojo.User; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.deserialize.MqttJsonDeserializer; +import org.dromara.mica.mqtt.core.annotation.MqttClientSubscribe; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * 客户端消息监听 + * + * @author L.cm + * @author ChangJin Wei(魏昌进) + */ +@Slf4j +@Service +public class MqttClientSubscribeListener { + + @MqttClientSubscribe("/test/#") + public void subQos0(String topic, byte[] payload) { + log.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + + @MqttClientSubscribe(value = "/qos1/#", qos = MqttQoS.QOS1) + public void subQos1(String topic, byte[] payload) { + log.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, Map data) { + // 2.4.5 开始支持,支持 2 到 3 个参数,字段类型映射规则(顺序)如下: + // String 字符串会默认映射到 topic, + // Map topicVars 会默认映射到 topic 中的变量解析(v2.5.4支持),注意:别跟消息序列化的冲突,消息反序列化不要用 Map + // MqttPublishMessage 会默认映射到 原始的消息,可以拿到 mqtt5 的 props 参数 + // byte[] 会映射到 mqtt 消息内容 payload + // ByteBuffer 会映射到 mqtt 消息内容 payload + // 其他类型会走序列化,确保消息能够序列化,默认为 json 序列化 + log.info("topic:{} json data:{}", topic, data); + } + + @MqttClientSubscribe(value = "/test/object") + public void testJson(String topic, MqttPublishMessage message, User data) { + log.info("topic:{} json data:{}", topic, data); + } + + /** + * 订阅,参数为可选,但是参数数量必须大于 2, + * + * @param topic topic 参数,可选参数 + * @param topicVars 订阅 topic 模板 ${productKey} 中的变量解析(v2.5.4支持),可选参数,注意:类型必须为 Map + * @param payload 消息内容,以字节数组形式提供,可选参数,也可支持对象形式,默认 json 序列化 + */ + @MqttClientSubscribe("/sys/${productKey}/${deviceName}/thing/sub/register") + public void thingSubRegister(String topic, Map topicVars, byte[] payload) { + // 1.3.8 开始支持,@MqttClientSubscribe 注解支持 ${} 变量替换,会默认替换成 + + // 注意:mica-mqtt 会先从 Spring boot 配置中替换参数 ${},如果存在配置会优先被替换。 + log.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8)); + } + + +} + diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/OtherMqttClientSubscribeListener.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/OtherMqttClientSubscribeListener.java new file mode 100644 index 0000000..c5be24f --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/listener/OtherMqttClientSubscribeListener.java @@ -0,0 +1,29 @@ +package org.dromara.mica.mqtt.client.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.annotation.MqttClientSubscribe; +import org.springframework.stereotype.Service; + +/** + * 客户端消息监听,注解在方法上 + * + * @author L.cm + */ +@Slf4j +@Service +public class OtherMqttClientSubscribeListener { + + @MqttClientSubscribe( + value = { + "$share/iothub/test/${a}", + "/test/${arg1}/${arg2}/${arg3}/${arg4}" + }, + clientTemplateBean = "mqttClientTemplate1" + ) + public void sub(String topic, MqttPublishMessage message, byte[] payload) { + log.info("topic:{} payload:{}", topic, new String(payload)); + } + +} + diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/pojo/User.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/pojo/User.java new file mode 100644 index 0000000..c3439b4 --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/pojo/User.java @@ -0,0 +1,9 @@ +package org.dromara.mica.mqtt.client.pojo; + +import lombok.Data; + +@Data +public class User { + private String name; + private T girlfriend; +} diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/ClientService.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/ClientService.java new file mode 100644 index 0000000..801773a --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/ClientService.java @@ -0,0 +1,52 @@ +package org.dromara.mica.mqtt.client.service; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.spring.client.MqttClientTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; + +/** + * @author wsq + */ +@Slf4j +@Service +public class ClientService { + /** + * 使用 默认的 mqtt client + */ + @Autowired + @Qualifier(MqttClientTemplate.DEFAULT_CLIENT_TEMPLATE_BEAN) + private MqttClientTemplate client; + + @Autowired + private HelloInterfaceA helloInterfaceA; + + @Autowired + private HelloInterfaceB helloInterfaceB; + + public boolean publish(String body) { + client.publish("/test/client", body.getBytes(StandardCharsets.UTF_8)); + return true; + } + + public boolean publishHelloInterfaceA(String body) { + helloInterfaceA.sayHello(body.getBytes(StandardCharsets.UTF_8)); + return true; + } + + public boolean publishHelloInterfaceB(String body) { + helloInterfaceB.sayHello(body.getBytes(StandardCharsets.UTF_8)); + return true; + } + + public boolean sub() { + client.subQos0("/test/#", (context, topic, message, payload) -> { + log.info("{}\t{}", topic, new String(payload, StandardCharsets.UTF_8)); + }); + return true; + } + +} diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/HelloInterfaceA.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/HelloInterfaceA.java new file mode 100644 index 0000000..2a1a3b2 --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/HelloInterfaceA.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.client.service; + +import org.dromara.mica.mqtt.core.annotation.MqttClientPublish; +import org.dromara.mica.mqtt.core.annotation.MqttPayload; +import org.dromara.mica.mqtt.spring.client.annotation.MqttClient; + +/** + * @author ChangJin Wei (魏昌进) + */ +@MqttClient +public interface HelloInterfaceA { + + @MqttClientPublish("/test/HelloInterfaceA") + void sayHello(@MqttPayload Object payload); +} diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/HelloInterfaceB.java b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/HelloInterfaceB.java new file mode 100644 index 0000000..a9b73de --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/java/org/dromara/mica/mqtt/client/service/HelloInterfaceB.java @@ -0,0 +1,32 @@ +/* + * 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.service; + +import org.dromara.mica.mqtt.core.annotation.MqttClientPublish; +import org.dromara.mica.mqtt.core.annotation.MqttPayload; +import org.dromara.mica.mqtt.spring.client.annotation.MqttClient; + +/** + * @author ChangJin Wei (魏昌进) + */ +@MqttClient +public interface HelloInterfaceB { + + @MqttClientPublish("/test/HelloInterfaceB") + void sayHello(@MqttPayload Object payload); + +} diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/resources/application.yml b/example/mica-mqtt-client-spring-boot-example/src/main/resources/application.yml new file mode 100644 index 0000000..eb84e26 --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/src/main/resources/application.yml @@ -0,0 +1,46 @@ +server: + port: 30012 +spring: + application: + name: mica-mqtt-client +# mqtt-client 配置 +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 + will-message: + topic: /test/offline + message: down + 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/# +springdoc: + swagger-ui: + urls: + - name: swagger + url: /v3/api-docs +logging: + level: + root: info + server: info # t-io 服务端默认日志 + org.tio: info # t-io 服务端默认日志 + org.dromara.mica.mqtt: info # mica-mqtt 日志 diff --git a/example/mica-mqtt-client-spring-boot-example/src/main/resources/banner.txt b/example/mica-mqtt-client-spring-boot-example/src/main/resources/banner.txt new file mode 100644 index 0000000..2eb7254 --- /dev/null +++ b/example/mica-mqtt-client-spring-boot-example/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/example/mica-mqtt-example/pom.xml b/example/mica-mqtt-example/pom.xml new file mode 100644 index 0000000..53a3d39 --- /dev/null +++ b/example/mica-mqtt-example/pom.xml @@ -0,0 +1,183 @@ + + + 4.0.0 + mica-mqtt-example + ${project.artifactId} + + + org.dromara.mica-mqtt + example + ${revision} + + + + 25.0.1 + org.dromara.mica.mqtt.server.MqttServerTest + org.dromara.mica.mqtt.client.MqttClientTest + + + + + org.dromara.mica-mqtt + mica-mqtt-client + + + org.dromara.mica-mqtt + mica-mqtt-server + + + com.google.code.gson + gson + + + + + ${project.artifactId} + + + + + jar + + true + + + + + org.tinylog + slf4j-tinylog + + + org.tinylog + tinylog-impl + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + org.apache.maven.plugins + maven-assembly-plugin + 3.8.0 + + + + ${mainClass.server} + + + + jar-with-dependencies + + false + + + + make-assembly + package + + single + + + + + + + + + mqtt-server-graal + + + + org.slf4j + slf4j-jdk14 + + + + org.graalvm.sdk + graal-sdk + ${graalvm.version} + provided + + + + mqtt-server-graal + + + org.graalvm.nativeimage + native-image-maven-plugin + 21.2.0 + + + + native-image + + package + + + + false + mqtt-server-graalvm + ${mainClass.server} + + -H:+RemoveSaturatedTypeFlows + --allow-incomplete-classpath + --no-fallback + + + + + + + + mqtt-client-graal + + + + org.slf4j + slf4j-jdk14 + + + + org.graalvm.sdk + graal-sdk + ${graalvm.version} + provided + + + + mqtt-client-graal + + + org.graalvm.nativeimage + native-image-maven-plugin + 21.2.0 + + + + native-image + + package + + + + false + mqtt-client-graalvm + ${mainClass.client} + + -H:+RemoveSaturatedTypeFlows + --allow-incomplete-classpath + --no-fallback + + + + + + + + + diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/aliyun/MqttClientTest.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/aliyun/MqttClientTest.java new file mode 100644 index 0000000..426ed5d --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/aliyun/MqttClientTest.java @@ -0,0 +1,65 @@ +/* + * 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.aliyun; + +import org.dromara.mica.mqtt.core.client.MqttClient; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 客户端测试 + * + * @author L.cm + */ +public class MqttClientTest { + + public static void main(String[] args) { + String productKey = "g27jB42P9hm"; + String deviceName = "3dbc1cb4"; + String deviceSecret = ""; + // 计算MQTT连接参数。 + MqttSign sign = new MqttSign(productKey, deviceName, deviceSecret); + + String username = sign.getUsername(); + String password = sign.getPassword(); + String clientId = sign.getClientId(); + System.out.println("username: " + username); + System.out.println("password: " + password); + System.out.println("clientid: " + clientId); + + // 初始化 mqtt 客户端 + MqttClient client = MqttClient.create() + .ip(productKey + ".iot-as-mqtt.cn-shanghai.aliyuncs.com") + .port(443) + .username(username) + .password(password) + .clientId(clientId) + .connectSync(); + + client.subQos0("/sys/" + productKey + '/' + deviceName + "/thing/event/property/post_reply", (context, topic, message, payload) -> { + System.out.println(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + }); + + client.schedule(() -> { + int LightSwitch = ThreadLocalRandom.current().nextBoolean() ? 0 : 1; + String content = "{\"id\":\"1\",\"version\":\"1.0\",\"params\":{\"LightSwitch\":" + LightSwitch + "}}"; + client.publish("/sys/" + productKey + "/" + deviceName + "/thing/event/property/post", content.getBytes(StandardCharsets.UTF_8)); + }, 3000); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/aliyun/MqttSign.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/aliyun/MqttSign.java new file mode 100644 index 0000000..999096a --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/aliyun/MqttSign.java @@ -0,0 +1,81 @@ +/* + * 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.aliyun; + +import org.tio.utils.mica.DigestUtils; + +import java.util.Objects; + +/** + * 阿里云 mqtt 签名方式 + * + * @author L.cm + */ +public class MqttSign { + /** + * 用户名 + */ + private final String username; + /** + * 密码 + */ + private final String password; + /** + * 客户端id + */ + private final String clientId; + + public MqttSign(String productKey, String deviceName, String deviceSecret) { + Objects.requireNonNull(productKey, "productKey is null"); + Objects.requireNonNull(deviceName, "deviceName is null"); + Objects.requireNonNull(deviceSecret, "deviceSecret is null"); + this.username = deviceName + '&' + productKey; + String timestamp = Long.toString(System.currentTimeMillis()); + this.password = getPassword(productKey, deviceName, deviceSecret, timestamp); + this.clientId = getClientId(productKey, deviceName, timestamp); + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getClientId() { + return clientId; + } + + private static String getPassword(String productKey, String deviceName, String deviceSecret, String timestamp) { + String plainPwd = "clientId" + productKey + '.' + deviceName + "deviceName" + + deviceName + "productKey" + productKey + "timestamp" + timestamp; + return hmacSha256(plainPwd, deviceSecret); + } + + private static String getClientId(String productKey, String deviceName, String timestamp) { + return productKey + '.' + deviceName + "|timestamp=" + timestamp + ",_v=paho-java-1.0.0,securemode=2,signmethod=hmacsha256|"; + } + + private static String hmacSha256(String plainText, String key) { + if (plainText == null || key == null) { + return null; + } + return DigestUtils.hmacSha256Hex(plainText, key); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttBenchmark.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttBenchmark.java new file mode 100644 index 0000000..b3e495f --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttBenchmark.java @@ -0,0 +1,72 @@ +/* + * 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.benchmark; + +import org.dromara.mica.mqtt.core.client.MqttClient; +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.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; + +/** + * mqtt 压力测试 + * + * @author L.cm + */ +public class MqttBenchmark { + + public static void main(String[] args) { + // 注意: windows 上需要修改最大的 Tcp 连接数,不然超不过 2W。 + // 《修改Windows服务器最大的Tcp连接数》:https://www.jianshu.com/p/00136a97d2d8 + int connCount = 5_0000; + String ip = "127.0.0.1"; + // 优化:使用ArrayList+预分配容量,性能比CopyOnWriteArrayList好很多 + // 对于压测场景,不需要线程安全的写操作(因为是单线程创建客户端) + final List clientList = new ArrayList<>(connCount); + SynThreadPoolExecutor tioExecutor = ThreadUtils.getTioExecutor(); + ExecutorService groupExecutor = ThreadUtils.getGroupExecutor(); + // 自定义全局 taskService,避免每个 client new,创建过多线程 + TimerTaskService taskService = new DefaultTimerTaskService(200L, 60); + for (int i = 0; i < connCount; i++) { + newClient(ip, i, clientList, tioExecutor, groupExecutor, taskService); + } + } + + private static void newClient(String ip, int i, final List clientList, + SynThreadPoolExecutor tioExecutor, + ExecutorService groupExecutor, + TimerTaskService taskService) { + MqttClient client = MqttClient.create() + .ip(ip) + .clientId(StrUtil.getNanoId() + i) + .readBufferSize(128) + // 取消自动重连 + .reconnect(false) + .tioExecutor(tioExecutor) + .groupExecutor(groupExecutor) + .mqttExecutor(tioExecutor) + .taskService(taskService) + .connect(); + clientList.add(client); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttPublishBench.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttPublishBench.java new file mode 100644 index 0000000..f7bf2d7 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttPublishBench.java @@ -0,0 +1,59 @@ +package org.dromara.mica.mqtt.benchmark; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.client.MqttClient; +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.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * mqtt 发布端测试 + * + * @author L.cm + */ +public class MqttPublishBench { + + public static void main(String[] args) { + int clientCount = 10; + int publishCount = 10000; + MqttQoS qos = MqttQoS.QOS0; + List clients = getClient(clientCount); + Executors.newScheduledThreadPool(ThreadUtils.AVAILABLE_PROCESSORS).scheduleWithFixedDelay(() -> { + for (MqttClient mqttClient : clients) { + for (int j = 0; j < publishCount; j++) { + byte[] payload = new byte[1024 + j]; + Arrays.fill(payload, (byte) -1); + mqttClient.publish("/topic/" + j, payload, qos); + } + } + }, 1L, 1L, TimeUnit.SECONDS); + } + + public static List getClient(int clientCount) { + SynThreadPoolExecutor tioExecutor = ThreadUtils.getTioExecutor(); + ExecutorService groupExecutor = ThreadUtils.getGroupExecutor(); + TimerTaskService taskService = new DefaultTimerTaskService(); + List clients = new ArrayList<>(); + for (int i = 0; i < clientCount; i++) { + MqttClient client = MqttClient.create() + .clientId(StrUtil.getNanoId()) + .tioExecutor(tioExecutor) + .groupExecutor(groupExecutor) + .mqttExecutor(groupExecutor) + .taskService(taskService) + .connectSync(); + clients.add(client); + } + return clients; + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttServerBench.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttServerBench.java new file mode 100644 index 0000000..6597352 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/benchmark/MqttServerBench.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.benchmark; + +import org.dromara.mica.mqtt.core.server.MqttServer; + +/** + * mqtt 服务端测试 + * + * @author L.cm + */ +public class MqttServerBench { + + public static void main(String[] args) { + // 设定日志级别为 error + System.setProperty("tinylog.writer.level", "error"); + // 启动 mqtt 服务 + MqttServer.create() + .enableMqtt() + .statEnable(false) + .start(); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceA.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceA.java new file mode 100644 index 0000000..c44ab75 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceA.java @@ -0,0 +1,47 @@ +/* + * 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.broker; + +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +/** + * 设备 A,这里默认 APP 应用端 + * + * @author L.cm + */ +public class DeviceA { + private static final Logger logger = LoggerFactory.getLogger(DeviceA.class); + + public static void main(String[] args) { + // 初始化 mqtt 客户端 + MqttClient client = MqttClient.create() + .ip("127.0.0.1") + .port(1883) + .username("admin") + .password("123456") + .connectSync(); + + client.subQos0("/a/door/open", (context, topic, message, payload) -> { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + }); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceB.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceB.java new file mode 100644 index 0000000..4f8aa6d --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceB.java @@ -0,0 +1,46 @@ +/* + * 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.broker; + +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +/** + * 设备 B,这里默认 web 端 + * + * @author L.cm + */ +public class DeviceB { + private static final Logger logger = LoggerFactory.getLogger(DeviceB.class); + + public static void main(String[] args) { + // 初始化 mqtt 客户端 + MqttClient client = MqttClient.create() + .ip("127.0.0.1") + .port(1883) + .username("admin") + .password("123456") + .connectSync(); + + client.subQos0("/a/door/open", (context, topic, message, payload) -> { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + }); + } +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceC.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceC.java new file mode 100644 index 0000000..13f0f26 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceC.java @@ -0,0 +1,43 @@ +/* + * 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.broker; + +import org.dromara.mica.mqtt.core.client.MqttClient; + +/** + * 设备 C,每 5 秒上报一个数据 + * + * @author L.cm + */ +public class DeviceC { + + public static void main(String[] args) { + // 初始化 mqtt 客户端 + MqttClient client = MqttClient.create() + .ip("127.0.0.1") + .port(1883) + .username("admin") + .password("123456") + .connectSync(); + + client.schedule(() -> { + client.publish("/a/door/open", null); + + }, 5000); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceD.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceD.java new file mode 100644 index 0000000..a47236e --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/DeviceD.java @@ -0,0 +1,36 @@ +package org.dromara.mica.mqtt.broker; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.annotation.MqttRetain; +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.dromara.mica.mqtt.core.annotation.MqttClientPublish; +import org.dromara.mica.mqtt.core.annotation.MqttPayload; + +/** + * @author ChangJin Wei (魏昌进) + */ +public class DeviceD { + + public static void main(String[] args) { + // 初始化 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/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/Server.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/Server.java new file mode 100644 index 0000000..0934bd1 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/broker/Server.java @@ -0,0 +1,42 @@ +/* + * 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.broker; + +import org.dromara.mica.mqtt.core.server.MqttServer; + +/** + * 服务端,单纯的做消息转发 + * + * @author L.cm + */ +public class Server { + + /** + * 客户端 A 模拟 APP 端订阅 `/a/door/open`, + * 客户端 B 模拟 web 网页端 mqtt.js 订阅 `/a/door/open`, + * Mqtt 服务端实现 `IMqttMessageListener`,将消息转交给 `AbstractMqttMessageDispatcher`(自定义实现)处理。 + * 客户端 C 定时上报转态给 `/a/door/open` + * 结果:A 和 B 将收到 C 或 D 发布的消息,并完成相应的效果展示。 + */ + public static void main(String[] args) { + // 启动服务,mica-mqtt 1.3.x 已经默认为 broker 模式 + MqttServer.create() + .enableMqtt() + .debug() + .start(); + } +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/Mqtt5ClientTest.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/Mqtt5ClientTest.java new file mode 100644 index 0000000..355dcd6 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/Mqtt5ClientTest.java @@ -0,0 +1,79 @@ +/* + * 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; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.codec.MqttVersion; +import org.dromara.mica.mqtt.core.client.IMqttClientMessageListener; +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +import java.nio.charset.StandardCharsets; + +/** + * 客户端测试 + * + * @author L.cm + */ +public class Mqtt5ClientTest { + private static final Logger logger = LoggerFactory.getLogger(Mqtt5ClientTest.class); + + public static void main(String[] args) { + // 初始化 mqtt 客户端 + MqttClient client = MqttClient.create() + .ip("127.0.0.1") + .port(1883) + .username("mica") + .password("mica") + .version(MqttVersion.MQTT_5) + .cleanStart(false) + .sessionExpiryIntervalSecs(7200) + .connectListener(new MqttClientConnectListener()) + .willMessage(builder -> { + builder.topic("/test/offline") + .messageText("down") + .retain(false) + .qos(MqttQoS.QOS0); // 遗嘱消息 + }) + // 同步连接,也可以使用 connect() 异步(可以避免 broker 没启动照成启动卡住),但是下面的订阅和发布可能还没连接成功。 + .connectSync(); + + client.subQos0("/test/123", new IMqttClientMessageListener() { + @Override + public void onSubscribed(ChannelContext context, String topicFilter, MqttQoS mqttQoS) { + // 订阅成功之后触发,可在此处做一些业务逻辑 + logger.info("topicFilter:{} MqttQoS:{} 订阅成功!!!", topicFilter, mqttQoS); + } + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + } + }); + + client.publish("/test/client", "mica最牛皮1".getBytes(StandardCharsets.UTF_8)); + client.publish("/test/client", "mica最牛皮2".getBytes(StandardCharsets.UTF_8)); + client.publish("/test/client", "mica最牛皮3".getBytes(StandardCharsets.UTF_8)); + + client.schedule(() -> { + client.publish("/test/client", "mica最牛皮".getBytes(StandardCharsets.UTF_8)); + }, 1000); + } +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientConnTest.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientConnTest.java new file mode 100644 index 0000000..4a6c376 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientConnTest.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; + +import org.dromara.mica.mqtt.codec.codes.MqttConnectReasonCode; +import org.dromara.mica.mqtt.core.client.MqttClient; + +/** + * 客户端测试 + * + * @author L.cm + */ +public class MqttClientConnTest { + + public static void main(String[] args) { + // 初始化 mqtt 客户端 + MqttConnectReasonCode reasonCode = MqttClient.create() +// .ip("127.0.0.1") + .ip("mqtt.dreamlu.net") + .port(1883) + .username("mica") + .password("mica1") + .connectTest(); + System.out.println(reasonCode); + } +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientConnectListener.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientConnectListener.java new file mode 100644 index 0000000..f5f72e5 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientConnectListener.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.client; + +import org.dromara.mica.mqtt.core.client.IMqttClientConnectListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +/** + * 客户端连接状态监听 + * + * @author L.cm + */ +public class MqttClientConnectListener implements IMqttClientConnectListener { + private static final Logger logger = LoggerFactory.getLogger(MqttClientConnectListener.class); + + @Override + public void onConnected(ChannelContext context, boolean isReconnect) { + String clientId = context.getId(); + if (isReconnect) { + logger.info("重连 mqtt 服务器重连成功... clientId:{}", clientId); + } else { + logger.info("连接 mqtt 服务器成功... clientId:{}", clientId); + } + } + + @Override + public void onDisconnect(ChannelContext context, Throwable throwable, String remark, boolean isRemove) { + String clientId = context.getId(); + logger.error("mqtt 链接断开 remark:{} isRemove:{} clientId:{}", remark, isRemove, clientId, throwable); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientGlobalTest.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientGlobalTest.java new file mode 100644 index 0000000..08abc30 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientGlobalTest.java @@ -0,0 +1,50 @@ +/* + * 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; + +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.tio.utils.buffer.ByteBufferUtil; + +/** + * 客户端全局订阅测试 + * + * @author L.cm + */ +public class MqttClientGlobalTest { + + public static void main(String[] args) { + // 初始化 mqtt 客户端 + MqttClient.create() + .ip("127.0.0.1") + .port(1883) + .username("admin") + .password("123456") + // 采用 globalSubscribe,保留 session 停机重启后,可以接受到离线消息,注意:clientId 要不能变化。 + .clientId("globalTest") + .cleanStart(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)); + }) +// .debug() + .connectSync(); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientSyncTest.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientSyncTest.java new file mode 100644 index 0000000..8897745 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientSyncTest.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; + +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +/** + * 客户端测试 + * + * @author L.cm + */ +public class MqttClientSyncTest { + private static final Logger logger = LoggerFactory.getLogger(MqttClientSyncTest.class); + + public static void main(String[] args) { + // 初始化 mqtt 客户端 + MqttClient client = MqttClient.create() + .ip("127.0.0.1") + .port(1883) + .username("mica") + .password("mica") + .connectListener(new MqttClientConnectListener()) + // 同步连接,注意:连接会阻塞 + .connectSync(); + + client.subQos0("/test/#", (context, topic, message, payload) -> { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + }); + + client.unSubscribe("/test/#", "/test/123"); + + // 连接上之后发送消息,注意:连接时出现异常等就不会发出 + client.publish("/test/client", "mica最牛皮".getBytes(StandardCharsets.UTF_8)); + // 2.3.0 开始支持,可停止 +// client.stop(); + } +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientTest.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientTest.java new file mode 100644 index 0000000..a61a66a --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/client/MqttClientTest.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.client; + +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.client.IMqttClientMessageListener; +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +import java.nio.charset.StandardCharsets; + +/** + * 客户端测试 + * + * @author L.cm + */ +public class MqttClientTest { + private static final Logger logger = LoggerFactory.getLogger(MqttClientTest.class); + + public static void main(String[] args) { + // 初始化 mqtt 客户端 + MqttClient client = MqttClient.create() + .ip("127.0.0.1") + .port(1883) + .username("mica") + .password("mica") + // 绑定网卡 v2.5.1 添加,支持 bindIp(网卡ip)和 bindNetworkInterface(网卡名)取一即可 +// .bindIp("127.0.0.1") +// .bindNetworkInterface("lo") +// 如果包体过大,建议将此参数设置和 maxBytesInMessage 一样大 +// .readBufferSize(1024 * 10) +// 最大包体长度,如果包体过大需要设置此参数 +// .maxBytesInMessage(1024 * 10) +// .version(MqttVersion.MQTT_5) +// 连接监听 + .connectListener(new MqttClientConnectListener()) + // 遗嘱消息 + .willMessage(builder -> { + builder.topic("/test/offline") + .messageText("down") + .retain(false) + .qos(MqttQoS.QOS0) + // mqtt5 遗嘱消息属性 + .willProperties(props -> { + props.setWillDelayInterval(1000); + props.setContentType("text/plain"); + }); + }) + // 同步连接,也可以使用 connect() 异步(可以避免 broker 没启动照成启动卡住),但是下面的订阅和发布可能还没连接成功。 + .connectSync(); + + client.subQos0("/test/#", new IMqttClientMessageListener() { + @Override + public void onSubscribed(ChannelContext context, String topicFilter, MqttQoS mqttQoS) { + // 订阅成功之后触发,可在此处做一些业务逻辑 + logger.info("topicFilter:{} MqttQoS:{} 订阅成功!!!", topicFilter, mqttQoS); + } + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + } + }); + + client.subQos0("/test/1", new IMqttClientMessageListener() { + @Override + public void onSubscribed(ChannelContext context, String topicFilter, MqttQoS mqttQoS) { + // 订阅成功之后触发,可在此处做一些业务逻辑 + logger.info("topicFilter:{} MqttQoS:{} 订阅成功!!!", topicFilter, mqttQoS); + } + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + } + }); + + client.subQos0("/test/2", new IMqttClientMessageListener() { + @Override + public void onSubscribed(ChannelContext context, String topicFilter, MqttQoS mqttQoS) { + // 订阅成功之后触发,可在此处做一些业务逻辑 + logger.info("topicFilter:{} MqttQoS:{} 订阅成功!!!", topicFilter, mqttQoS); + } + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + } + }); + + client.subQos0("/test/3", new IMqttClientMessageListener() { + @Override + public void onSubscribed(ChannelContext context, String topicFilter, MqttQoS mqttQoS) { + // 订阅成功之后触发,可在此处做一些业务逻辑 + logger.info("topicFilter:{} MqttQoS:{} 订阅成功!!!", topicFilter, mqttQoS); + } + + @Override + public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) { + logger.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + } + }); + + client.publish("/test/client", "mica最牛皮1".getBytes(StandardCharsets.UTF_8)); + client.publish("/test/client", "mica最牛皮2".getBytes(StandardCharsets.UTF_8)); + client.publish("/test/client", "mica最牛皮3".getBytes(StandardCharsets.UTF_8)); + + client.schedule(() -> { + client.publish("/test/client", "mica最牛皮".getBytes(StandardCharsets.UTF_8)); + }, 2000); + } +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/huawei/MqttClientTest.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/huawei/MqttClientTest.java new file mode 100644 index 0000000..cd15e04 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/huawei/MqttClientTest.java @@ -0,0 +1,70 @@ +/* + * 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.huawei; + +import org.dromara.mica.mqtt.core.client.MqttClient; + +import java.nio.charset.StandardCharsets; + +/** + * 客户端测试 + * + * @author L.cm + */ +public class MqttClientTest { + + public static void main(String[] args) { + // 设备id和密钥,请从华为云iot获取 + String deviceId = "630eb6f8664c6f7938db6ef0_test"; + String deviceSecret = ""; + // 计算MQTT连接参数。 + MqttSign sign = new MqttSign(deviceId, deviceSecret); + + String username = sign.getUsername(); + String password = sign.getPassword(); + String clientId = sign.getClientId(); + System.out.println("username: " + username); + System.out.println("password: " + password); + System.out.println("clientid: " + clientId); + + // 初始化 mqtt 客户端 + MqttClient client = MqttClient.create() + .ip("iot-mqtts.cn-north-4.myhuaweicloud.com") + .port(8883) + .username(username) + .password(password) + .clientId(clientId) + .useSsl() + .connectSync(); + + // 订阅命令下发topic + String cmdRequestTopic = "$oc/devices/" + deviceId + "/sys/commands/#"; + + client.subQos0(cmdRequestTopic, (context, topic, message, payload) -> { + System.out.println(topic + '\t' + new String(payload, StandardCharsets.UTF_8)); + }); + + // 属性上报消息 + String reportTopic = "$oc/devices/" + deviceId + "/sys/properties/report"; + String jsonMsg = "{\"services\":[{\"service_id\":\"Temperature\", \"properties\":{\"value\":57}},{\"service_id\":\"Battery\",\"properties\":{\"level\":88}}]}"; + + client.schedule(() -> { + client.publish(reportTopic, jsonMsg.getBytes(StandardCharsets.UTF_8)); + }, 3000); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/huawei/MqttSign.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/huawei/MqttSign.java new file mode 100644 index 0000000..9766d3a --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/huawei/MqttSign.java @@ -0,0 +1,89 @@ +/* + * 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.huawei; + +import org.tio.utils.mica.DigestUtils; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +/** + * 华为云 mqtt 签名方式 + * + * @author L.cm + */ +public class MqttSign { + /** + * 用户名 + */ + private final String username; + /** + * 密码 + */ + private final String password; + /** + * 客户端id + */ + private final String clientId; + + public MqttSign(String deviceId, String deviceSecret) { + Objects.requireNonNull(deviceId, "deviceId is null"); + Objects.requireNonNull(deviceSecret, "deviceSecret is null"); + this.username = deviceId; + String timestamp = getTimeStamp(); + this.password = getPassword(deviceSecret, timestamp); + this.clientId = getClientId(deviceId, timestamp); + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getClientId() { + return clientId; + } + + private static String getPassword(String deviceSecret, String timestamp) { + return hmacSha256(deviceSecret, timestamp); + } + + private static String getClientId(String deviceId, String timestamp) { + return deviceId + "_0_0_" + timestamp; + } + + /*** + * 要求:10位数字 + */ + private static String getTimeStamp() { + return ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).format(DateTimeFormatter.ofPattern("yyyyMMddHH")); + } + + /*** + * 调用sha256算法进行哈希 + */ + private static String hmacSha256(String message, String tStamp) { + return DigestUtils.hmacSha256Hex(message, tStamp); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/nginx/MqttServerProxyProtocol.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/nginx/MqttServerProxyProtocol.java new file mode 100644 index 0000000..2722d32 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/nginx/MqttServerProxyProtocol.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.nginx; + + +import org.dromara.mica.mqtt.core.server.MqttServer; + +/** + * mqtt 服务端测试 + * + * @author L.cm + */ +public class MqttServerProxyProtocol { + + public static void main(String[] args) { + MqttServer.create() + .enableMqtt() + .enableMqttWs() + // 开启代理协议 + .proxyProtocolEnable() + .statEnable(false) + .debug() + .start(); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/proxy/MqttClientProxy.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/proxy/MqttClientProxy.java new file mode 100644 index 0000000..d2b3482 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/proxy/MqttClientProxy.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.proxy; + +import org.dromara.mica.mqtt.codec.MqttQoS; +import org.dromara.mica.mqtt.core.client.MqttClient; + +/** + * 2 个 mqtt 服务间,使用 2 个 client 做数据传输 + * + * @author L.cm + */ +public class MqttClientProxy { + + public static void main(String[] args) { + MqttClient client1 = MqttClient.create() + .ip("ip1") + .port(1883) + .clientId("clientI") + .username("mica") + .password("mica") + .debug() + .connectSync(); + + MqttClient client2 = MqttClient.create() + .ip("ip2") + .port(1883) + .clientId("client2") + .username("mica") + .password("mica") + .debug() + .connectSync(); + + String[] topics = new String[]{ + "$share/test/link/product1/+/event/+/post", + "$share/test/link/product2/+/event/+/post", + "$share/test/link/product3/+/event/+/post" + }; + client1.subscribe(topics, MqttQoS.QOS0, (context, topic, message, payload) -> { + client2.publish(topic, payload); + }); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/proxy/MqttServerProxy.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/proxy/MqttServerProxy.java new file mode 100644 index 0000000..7026745 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/proxy/MqttServerProxy.java @@ -0,0 +1,67 @@ +/* + * 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.proxy; + +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.server.MqttConnectStatusListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.utils.buffer.ByteBufferUtil; + +/** + * mqtt 服务端代理到另外一个服务端 + * + * @author L.cm + */ +public class MqttServerProxy { + private static final Logger logger = LoggerFactory.getLogger(MqttServerProxy.class); + + public static void main(String[] args) { + // 需要将数据发往的服务端 + MqttClient client = MqttClient.create() + .ip("ip") + .port(1883) + .clientId("proxy") + .username("mica") + .password("mcia") + .debug() + .connectSync(); + // 接受数据的服务端 + MqttServer.create() + .messageListener((context, clientId, topic, qoS, message) -> { + byte[] payload = message.payload(); + logger.info("clientId:{} topic:{} payload:\n{}", clientId, topic, ByteBufferUtil.toString(payload)); + // 转发数据 + client.publish(topic, payload); + }) + // 开启 mqtt tcp 协议 + .enableMqtt() + // 开启 mqtt websocket + .enableMqttWs() + // 开启 http api 接口 + .enableMqttHttpApi() + // 客户端连接状态监听 + .connectStatusListener(new MqttConnectStatusListener()) + // 开始 stat 监控 + .statEnable() + // 开启 debug 信息日志 + .debug() + .start(); + } + +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttConnectStatusListener.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttConnectStatusListener.java new file mode 100644 index 0000000..e094afe --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttConnectStatusListener.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.server; + +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; + +/** + * mqtt 连接状态 + * + * @author L.cm + */ +public class MqttConnectStatusListener implements IMqttConnectStatusListener { + private static final Logger logger = LoggerFactory.getLogger(MqttConnectStatusListener.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/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttMessageInterceptor.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttMessageInterceptor.java new file mode 100644 index 0000000..55e693c --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttMessageInterceptor.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.server; + +import org.dromara.mica.mqtt.codec.message.MqttMessage; +import org.dromara.mica.mqtt.core.server.interceptor.IMqttMessageInterceptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.core.ChannelContext; +import org.tio.core.Node; + +/** + * mqtt 消息拦截器 + * + * @author L.cm + */ +public class MqttMessageInterceptor implements IMqttMessageInterceptor { + private static final Logger logger = LoggerFactory.getLogger(MqttMessageInterceptor.class); + + @Override + public void onAfterReceivedBytes(ChannelContext context, int receivedBytes) throws Exception { + // 注意:此时 clientId 可能为空 + String clientId = context.getBsId(); + Node clientNode = context.getClientNode(); + // ChannelStat channelStat = context.stat; + // 自定义规则,超限是可用 Tio.remove(context, "xxx超限"); 断开连接。 + logger.info("===接收 client:{} clientId:{} data:{}b", clientNode, clientId, receivedBytes); + } + + @Override + public void onAfterDecoded(ChannelContext context, MqttMessage message, int packetSize) { + // 注意:此时 clientId 可能为空 + String clientId = context.getBsId(); + Node clientNode = context.getClientNode(); + logger.info("===解码 client:{} clientId:{} message:{}", clientNode, clientId, message); + } + + @Override + public void onAfterHandled(ChannelContext context, MqttMessage message, long cost) throws Exception { + String clientId = context.getBsId(); + Node clientNode = context.getClientNode(); + logger.info("===处理完成 ip:{} clientId:{} message:{} 耗时:{}", clientNode, clientId, message, cost); + } +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttServerTest.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttServerTest.java new file mode 100644 index 0000000..84003b1 --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/server/MqttServerTest.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.server; + +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +/** + * mqtt 服务端测试 + * + * @author L.cm + */ +public class MqttServerTest { + private static final Logger logger = LoggerFactory.getLogger(MqttServerTest.class); + + public static void main(String[] args) { + // 注意:为了能接受更多链接(降低内存),请添加 jvm 参数 -Xss129k + MqttServer mqttServer = MqttServer.create() + // 服务端 ip 默认为空,0.0.0.0,建议不要设置,端口 默认:1883 + .enableMqtt(1883) + // 默认为: 8192(mqtt 默认最大消息大小),为了降低内存可以减小小此参数,如果消息过大 t-io 会尝试解析多次(建议根据实际业务情况而定) + .readBufferSize(8192) +// 最大包体长度 +// .maxBytesInMessage(1024 * 100) +// mqtt 3.1 协议会校验 clientId 长度。 +// .maxClientIdLength(64) + .messageListener((context, clientId, topic, qos, message) -> { + logger.info("clientId:{} payload:{}", clientId, new String(message.payload(), StandardCharsets.UTF_8)); + }) + // 客户端连接状态监听 + .connectStatusListener(new MqttConnectStatusListener()) + // 自定义消息拦截器 + .addInterceptor(new MqttMessageInterceptor()) + // 开启 websocket + .enableMqttWs() + // 开启 mqtt http 接口 + .enableMqttHttpApi(builder -> + builder + .basicAuth("mica", "mica") // http basic 认证 + .mcpServer() // 开启 mcp 服务 + .build() + ) + // 开始 stat 监控 + .statEnable() + // 开启 debug 信息日志 + .debug() + .start(); + + mqttServer.schedule(() -> { + String message = "mica最牛皮 " + System.currentTimeMillis(); + mqttServer.publishAll("/test/123", message.getBytes(StandardCharsets.UTF_8)); + }, 2000); + + // 2.3.2 开始支持 stop 关闭 +// mqttServer.stop(); + } +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/ssl/SslMqttClientTest.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/ssl/SslMqttClientTest.java new file mode 100644 index 0000000..2940d9a --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/ssl/SslMqttClientTest.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.ssl; + +import org.dromara.mica.mqtt.core.client.MqttClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.utils.buffer.ByteBufferUtil; + +import java.nio.charset.StandardCharsets; + +/** + * 客户端测试 + * + * @author L.cm + */ +public class SslMqttClientTest { + private static final Logger logger = LoggerFactory.getLogger(SslMqttClientTest.class); + + public static void main(String[] args) { + // 初始化 mqtt 客户端 + MqttClient client = MqttClient.create() + .ip("127.0.0.1") + .port(8883) + .username("mica") + .password("mica") + .useSsl("classpath:ssl/dreamlu.net.jks", "123456") + .connectSync(); + + client.subQos0("/test/#", (context, topic, message, payload) -> { + logger.info(topic + '\t' + ByteBufferUtil.toString(payload)); + }); + + // 定时发送数据 + client.schedule(() -> { + String message = "mica最牛皮 " + System.currentTimeMillis(); + client.publish("/test/123", message.getBytes(StandardCharsets.UTF_8)); + }, 5000); + } +} diff --git a/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/ssl/SslMqttServerTest.java b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/ssl/SslMqttServerTest.java new file mode 100644 index 0000000..3f19cde --- /dev/null +++ b/example/mica-mqtt-example/src/main/java/org/dromara/mica/mqtt/ssl/SslMqttServerTest.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.ssl; + +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.server.MqttConnectStatusListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tio.utils.buffer.ByteBufferUtil; + +import java.nio.charset.StandardCharsets; + +/** + * mqtt 服务端测试 + * + * @author L.cm + */ +public class SslMqttServerTest { + private static final Logger logger = LoggerFactory.getLogger(SslMqttServerTest.class); + + public static void main(String[] args) { + MqttServer mqttServer = MqttServer.create() + .enableMqttSsl(builder -> + builder.useSsl("classpath:ssl/dreamlu.net.jks", "123456") + .build() + ) + .messageListener((context, clientId, topic, qoS, message) -> { + logger.info("clientId:{} message:{} payload:{}", clientId, message, ByteBufferUtil.toString(message.payload())); + }) + .connectStatusListener(new MqttConnectStatusListener()) + .debug() + .start(); + + // 定时发送数据 + mqttServer.schedule(() -> { + String message = "mica最牛皮 " + System.currentTimeMillis(); + mqttServer.publishAll("/test/123", message.getBytes(StandardCharsets.UTF_8)); + }, 5000); + } +} diff --git a/example/mica-mqtt-example/src/main/resources/ssl/README.md b/example/mica-mqtt-example/src/main/resources/ssl/README.md new file mode 100644 index 0000000..97ff3c5 --- /dev/null +++ b/example/mica-mqtt-example/src/main/resources/ssl/README.md @@ -0,0 +1 @@ +ssl 自签双向证书详见:https://gitee.com/596392912/mica-mqtt/issues/I45GO7 \ No newline at end of file diff --git a/example/mica-mqtt-example/src/main/resources/ssl/ca-cert.pem b/example/mica-mqtt-example/src/main/resources/ssl/ca-cert.pem new file mode 100644 index 0000000..02a10df --- /dev/null +++ b/example/mica-mqtt-example/src/main/resources/ssl/ca-cert.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICejCCAeMCFGkzqgl8ilICj+PRCwIiUy3dPNuWMA0GCSqGSIb3DQEBCwUAMHwx +CzAJBgNVBAYTAkNOMQswCQYDVQQIDAJITjELMAkGA1UEBwwCQ1MxDTALBgNVBAoM +BFJNSlMxDTALBgNVBAsMBFJNSlMxFDASBgNVBAMMC2RyZWFtbHUubmV0MR8wHQYJ +KoZIhvcNAQkBFhA1OTYzOTI5MTJAcXEuY29tMB4XDTIyMDkwMzA4MjExNVoXDTMy +MDgzMTA4MjExNVowfDELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkhOMQswCQYDVQQH +DAJDUzENMAsGA1UECgwEUk1KUzENMAsGA1UECwwEUk1KUzEUMBIGA1UEAwwLZHJl +YW1sdS5uZXQxHzAdBgkqhkiG9w0BCQEWEDU5NjM5MjkxMkBxcS5jb20wgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAJxanDadRHd+D9jH5/mq7Pn3Fl915CiOaaJr +QS0ZxCqRtTaS9+JkFQJ4TnAezlKx/xDZu7G9pk7CJ6w+JQwfI+AwsAkOlrynyFbe +Hc9s6DZyZHXxkpgeQUqpnrkXkWG+jbh2aulWB4smQE/vPnpkjVGEe86+/JsYW3Sm +rFhL2xddAgMBAAEwDQYJKoZIhvcNAQELBQADgYEAP39PM0XpNzg3Bne63oXHRWDJ +bafiCwloO/SaxH7JtCBj1I05W3owwHSqWYadK+lg//tKf6TL+94GtW8s2VtpLGu7 +Y5R2aakhywzbWVfrEK+kyvTG/4nP9tvcKwh8Iqr2XlllDLfsQeyLacb4+pcfDY3G +8nrbe0lBKepae8D0SvQ= +-----END CERTIFICATE----- diff --git a/example/mica-mqtt-example/src/main/resources/ssl/client-cert.pem b/example/mica-mqtt-example/src/main/resources/ssl/client-cert.pem new file mode 100644 index 0000000..3d4d140 --- /dev/null +++ b/example/mica-mqtt-example/src/main/resources/ssl/client-cert.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICejCCAeMCFBebamYN4nfF0CrZWuN68SXxTH/GMA0GCSqGSIb3DQEBCwUAMHwx +CzAJBgNVBAYTAkNOMQswCQYDVQQIDAJITjELMAkGA1UEBwwCQ1MxDTALBgNVBAoM +BFJNSlMxDTALBgNVBAsMBFJNSlMxFDASBgNVBAMMC2RyZWFtbHUubmV0MR8wHQYJ +KoZIhvcNAQkBFhA1OTYzOTI5MTJAcXEuY29tMB4XDTIyMDkwMzA4MjM1N1oXDTMy +MDgzMTA4MjM1N1owfDELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkhOMQswCQYDVQQH +DAJDUzENMAsGA1UECgwEUk1KUzENMAsGA1UECwwEUk1KUzEUMBIGA1UEAwwLZHJl +YW1sdS5uZXQxHzAdBgkqhkiG9w0BCQEWEDU5NjM5MjkxMkBxcS5jb20wgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAL3hH0vRYCsqs79ghUPskgr+bQyVkO/6U1H1 +4yHJWyWeXT2tl+VTvXhou0Zk3yYtd6O3l241PgmAjwBDPyfmM2UU/zluXQzELEuf +AnC4Q2wl0vpoj7mGQeMuRpjZN3FblS0bRifvctx3ubWVvSzCFIaKLZgdrnyWm5Lm +kJQjRaEDAgMBAAEwDQYJKoZIhvcNAQELBQADgYEAkm/Mv9nEGE4xy3L4NmwgyZMo +j5CuarbCEv/MgXST8B/dJvtC7PbCzrb9BbUkiIUjCOnRC+2U0DbSAkHy7CCwHTLx +g9Syn1SMWn9SF74l74qVtuaRjMKCGwSlqBLH5/t1FdPoevY2a+zi5dAqwIhkjY84 +uYOgxODe+kFBUbT6W3A= +-----END CERTIFICATE----- diff --git a/example/mica-mqtt-example/src/main/resources/ssl/client-key.pem b/example/mica-mqtt-example/src/main/resources/ssl/client-key.pem new file mode 100644 index 0000000..429c7d3 --- /dev/null +++ b/example/mica-mqtt-example/src/main/resources/ssl/client-key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQC94R9L0WArKrO/YIVD7JIK/m0MlZDv+lNR9eMhyVslnl09rZfl +U714aLtGZN8mLXejt5duNT4JgI8AQz8n5jNlFP85bl0MxCxLnwJwuENsJdL6aI+5 +hkHjLkaY2TdxW5UtG0Yn73Lcd7m1lb0swhSGii2YHa58lpuS5pCUI0WhAwIDAQAB +AoGAR/T8azsZesJgA/KMDkWkws3QfahgmNEAqlrIjJFGHWd6ZllW6u1lLDBkaDTp +7AnnAQAePwGmVOuHRc42LOSsLMX/D2wYsTGjLTT1w+fEDkQCVnDKV8ZYWA5fN7Zh +m5cLB5IB23L/Xvs8UMYQ8qWufv6BxVPr+cawtXOK3O91AoECQQD5PwGSrMpsNkVG +Ox1t1A9wVgelbq+9qANPl8TvdaZKvNsbWwI0PRqK7D1SJXJiMX4m/hc8YkIXJWsO +Wd1yLg6TAkEAwwZLncJYlYOp0v8PXk75rDR7KV9fdBPPOdnZ1XU/yFW3anwOt0OX +LvGL3X0A2f/tqOX9v+3LENCICF4gAv850QJBALo7Yal+giEoy8oWEX8mnAKLxVrO +wXEsQI0QEY36ki31vqFJ9vOhVFvI+GiQok7MPD5WTHZJ1KgGxV8LtnLCBxECQCPD +WcZ6RyhT1qaco0LWFK7hiNxTYvu0TkH7kxizwZiJL3NVgJVWzbiMDuv06l0Ps5NP +abLydlSFCQ0PxasHBqECQQC0lhmqkNbaN3lJyGDoEWm6Kb9z3eh3+9Fk4j328aYW +gQcBeRhKU8kdDTg2flOWS3sxrEysJYv8i9DPbX9RsRFd +-----END RSA PRIVATE KEY----- diff --git a/example/mica-mqtt-example/src/main/resources/ssl/dreamlu.net.jks b/example/mica-mqtt-example/src/main/resources/ssl/dreamlu.net.jks new file mode 100644 index 0000000000000000000000000000000000000000..92f7b93eadb4afda53b542952f111e36b34968a3 GIT binary patch literal 1414 zcmezO_TO6u1_mY|W&~sIl%mwc+?-Oqywnn)STj@cE_+k1mue#J*y5@f2?8;4ZeL|%E3cAgoS*y%6V{*->_&A$cd$rkjEb89pYxYS!cA4$w zHoptI6azPx*S*zC`O>N>JUw+wf%Be7^`9yyRy9~06fgI(PF<5zZ@e!kRY1JE;AK=@ zky+(!h0tt~{YKHvf(?gf+sG``*5wP#&NC@;Jkhml-Zk%@!&-KqOMN)iWBgwIEvfRV z-*QpQ=^fX~Ljg=&?muD@dY>onO}yKyqp-Um!}sH&y8d4`Q~n3|27Ou+FOOnRl`uhXvsxi)1i^juUJlrzP3?SG4n@txImaY`3+ zdjflpNVJvn>px|=zfIxCs;{5+v3@=7%#Lm~X_l!)b-?iP=w2JA^{k!vh zGMHXmJ+a}V{rAfTTdN~>2v1Etd}Gs-Nc&TabHX#WZhCV&W5pV0VVN!JCr*p3S6!-g zsBf8>?i`OlcZ?z@Zr)QEvb)gTa#iZbb4RT*n|q@VJ8(o8E(=}o&-J^i+vPpQQlCC} z3awjcxH#~ST&c^IiYb9eRmBcc||+E&Q^ozZdV;6ZQ2{J+!Z zofTS8cultQ^GcSIGym32oACLMYT;^r#m3n!s~@FJl+`}yTCw-xXURo&9buKlR~bLN zneEcDp?s2}LH@4G&-Sv%RMjh-5SaU!LHl%}Z{I4n*emB!wGSI^ymN+m2{>W1M(CLu zSOSxB9WW_Z88k6IW)cyfot4J>sQl;!t(#Ggt3IlJ^r=5;z{|#|)#lOmotKf3o0Y+! z#*o{9lZ`o)g-w{r*$>9y;9>HBbJ%&9oP!N{4Y)zdxOiBCe7)dYZYWp8KnNtu%!8c4 z4CM`E;kI!yiV2unni*RfSsEHS6c*|w=jR&8iSrs68CV(^8(0__nVLt5^BNl&SQr~Z zxdZIF#`(yh1uPMm8+#cH8atU98yQaIrLUOu&G~lev}MZ=onvV9(0*;FGDqBf^X!(_ z4)2faANaU3Wy>GE&MV1GZg$82`ei>k5ggpM*ZII7zKh&(OJ;3f_~X@ zGd40YGcqtDI}aF{%s_WJFjjhJubw#bb$Xu5(VFSCof~gAZB99oHnpriK6}4z%)?D{ zJg;X(Khk0AxOZ)V<-22Ne*7?GXW4pJ!XWn+a literal 0 HcmV?d00001 diff --git a/example/mica-mqtt-example/src/main/resources/tinylog.properties b/example/mica-mqtt-example/src/main/resources/tinylog.properties new file mode 100644 index 0000000..571bd73 --- /dev/null +++ b/example/mica-mqtt-example/src/main/resources/tinylog.properties @@ -0,0 +1,8 @@ +writer = console +writer.format = {date: HH:mm:ss.SSS} [{thread}] {level} {class-name}.{method} : {message} +writer.level = info +# level +level@org.tio = warn +level@org.tio.client.TioClient = off +level@org.tio.server = info +level@org.dromara.mica.mqtt = info diff --git a/example/mica-mqtt-example/src/test/java/net/dreamlu/iot/package-info.java b/example/mica-mqtt-example/src/test/java/net/dreamlu/iot/package-info.java new file mode 100644 index 0000000..247a0c9 --- /dev/null +++ b/example/mica-mqtt-example/src/test/java/net/dreamlu/iot/package-info.java @@ -0,0 +1 @@ +package net.dreamlu.iot; diff --git a/example/mica-mqtt-example/src/test/resources/test.yml b/example/mica-mqtt-example/src/test/resources/test.yml new file mode 100644 index 0000000..e69de29 diff --git a/example/mica-mqtt-server-solon-plugin-example/pom.xml b/example/mica-mqtt-server-solon-plugin-example/pom.xml new file mode 100644 index 0000000..0699a06 --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + example + ${revision} + + + mica-mqtt-server-solon-plugin-example + + + + org.noear + solon-web + + + org.dromara.mica-mqtt + mica-mqtt-server-solon-plugin + + + + org.noear + solon-scheduling-simple + + + org.noear + solon-logging-simple + + + + org.noear + solon-cloud-metrics + + + io.micrometer + micrometer-registry-prometheus + + + + org.projectlombok + lombok + provided + + + + diff --git a/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/MqttServerApplication.java b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/MqttServerApplication.java new file mode 100644 index 0000000..e5ced68 --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/MqttServerApplication.java @@ -0,0 +1,19 @@ +package org.dromara.mica.mqtt.server.solon; + + +import org.noear.solon.Solon; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.scheduling.annotation.EnableScheduling; + +/** + * @author wsq + */ +@Configuration +@EnableScheduling +public class MqttServerApplication { + + public static void main(String[] args) { + Solon.start(MqttServerApplication.class, args); + } + +} diff --git a/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/controller/ServerController.java b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/controller/ServerController.java new file mode 100644 index 0000000..f28ff11 --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/controller/ServerController.java @@ -0,0 +1,22 @@ +package org.dromara.mica.mqtt.server.solon.controller; + + +import org.dromara.mica.mqtt.server.solon.service.ServerService; +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Inject; +import org.noear.solon.annotation.Mapping; +import org.noear.solon.annotation.Post; + +@Mapping("/mqtt/server") +@Controller +public class ServerController { + @Inject + private ServerService service; + + @Mapping("publish") + @Post + public boolean publish(String body) { + return service.publish(body); + } + +} diff --git a/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttConnectOfflineListener.java b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttConnectOfflineListener.java new file mode 100644 index 0000000..1c7ad35 --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttConnectOfflineListener.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.server.solon.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.server.solon.event.MqttClientOfflineEvent; +import org.noear.solon.annotation.Component; +import org.noear.solon.core.event.EventListener; + +/** + * mqtt 连接状态,使用 solon event 方式,性能有损耗 + * + * @author L.cm + */ +@Slf4j +@Component +public class MqttConnectOfflineListener implements EventListener { + + @Override + public void onEvent(MqttClientOfflineEvent mqttClientOfflineEvent) throws Throwable { + log.info("MqttClientOnlineEvent:{}", mqttClientOfflineEvent); + } +} diff --git a/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttConnectOnlineListener.java b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttConnectOnlineListener.java new file mode 100644 index 0000000..1b81cd7 --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttConnectOnlineListener.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.server.solon.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.server.solon.event.MqttClientOnlineEvent; +import org.noear.solon.annotation.Component; +import org.noear.solon.core.event.EventListener; + +/** + * mqtt 连接状态,使用 solon event 方式,性能有损耗 + * + * @author L.cm + */ +@Slf4j +@Component +public class MqttConnectOnlineListener implements EventListener { + + @Override + public void onEvent(MqttClientOnlineEvent mqttClientOnlineEvent) throws Throwable { + log.info("MqttClientOnlineEvent:{}", mqttClientOnlineEvent); + } +} diff --git a/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttServerMessageListener1.java b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttServerMessageListener1.java new file mode 100644 index 0000000..48523ca --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttServerMessageListener1.java @@ -0,0 +1,25 @@ +package org.dromara.mica.mqtt.server.solon.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.tio.core.ChannelContext; + +import java.nio.charset.StandardCharsets; + +/** + * 消息监听器示例1,直接实现 IMqttMessageListener,注意:如果实现了 IMqttMessageListener,MqttServerFunction 注解就不生效了。 + * + * @author wsq + */ +@Slf4j +//@Component +public class MqttServerMessageListener1 implements IMqttMessageListener { + + @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)); + } + +} diff --git a/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttServerMessageListener2.java b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttServerMessageListener2.java new file mode 100644 index 0000000..7b31709 --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/listener/MqttServerMessageListener2.java @@ -0,0 +1,48 @@ +package org.dromara.mica.mqtt.server.solon.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.codec.message.MqttPublishMessage; +import org.dromara.mica.mqtt.core.annotation.MqttServerFunction; +import org.dromara.mica.mqtt.server.solon.pojo.User; +import org.noear.solon.annotation.Component; +import org.tio.core.ChannelContext; +import org.tio.core.Node; + +import java.util.Map; + +/** + * 消息监听器示例2,MqttServerFunction 注解订阅,注意:如果自行实现了 IMqttMessageListener,MqttServerFunction 注解就不生效了。 + * + * @author L.cm + */ +@Slf4j +@Component +public class MqttServerMessageListener2 { + + @MqttServerFunction("/test/object") + public void func1(String topic, User user) { + log.info("topic:{} user:{}", topic, user); + } + + @MqttServerFunction("/test/client") + public void func1(String topic, byte[] message) { + log.info("topic:{} message:{}", topic, new String(message)); + } + + /** + * MQTT消息处理函数,匹配 mqtt Topic /test/+,如何需要匹配所以消息,请使用通配符 # + * + * @param context ChannelContext,可选参数 + * @param topic 实际接收到消息的主题名称,可选参数 + * @param topicVars topic 中的 ${xxxx} 变量解析(v2.5.4支持),可选参数,注意:类型必须为 Map + * @param publishMessage 完整的MQTT发布消息对象,包含消息头和负载,可选参数 + * @param message 消息负载内容,以字节数组形式提供,可选参数,也可支持对象形式,默认 json 序列化 + */ + @MqttServerFunction("/test/${xxxx}") + public void func3(ChannelContext context, String topic, Map topicVars, MqttPublishMessage publishMessage, byte[] message) { + // 获取客户端节点信息 + Node clientNode = context.getClientNode(); + // 记录接收到的MQTT消息信息 + log.info("clientNode:{} topic:{} topicVars:{} publishMessage:{} message:{}", clientNode, topic, topicVars, publishMessage, new String(message)); + } +} diff --git a/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/pojo/User.java b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/pojo/User.java new file mode 100644 index 0000000..c1d7f3c --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/pojo/User.java @@ -0,0 +1,19 @@ +package org.dromara.mica.mqtt.server.solon.pojo; + +import lombok.Data; + +@Data +public class User { + private String name; + private T girlfriend; + + public static User newUser(){ + User user1 = new User(); + user1.setName("name1"); + + User user2 = new User(); + user2.setName("name2"); + user2.setGirlfriend(user1); + return user2; + } +} diff --git a/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/service/ServerService.java b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/service/ServerService.java new file mode 100644 index 0000000..76da077 --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/service/ServerService.java @@ -0,0 +1,24 @@ +package org.dromara.mica.mqtt.server.solon.service; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.server.solon.MqttServerTemplate; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; + +import java.nio.charset.StandardCharsets; + +/** + * @author wsq + */ +@Slf4j +@Component +public class ServerService { + @Inject + private MqttServerTemplate server; + + public boolean publish(String body) { + boolean result = server.publishAll("/test/123", body.getBytes(StandardCharsets.UTF_8)); + log.info("Mqtt publishAll result:{}", result); + return result; + } +} diff --git a/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/task/PublishTask.java b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/task/PublishTask.java new file mode 100644 index 0000000..196df4d --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/src/main/java/org/dromara/mica/mqtt/server/solon/task/PublishTask.java @@ -0,0 +1,25 @@ +package org.dromara.mica.mqtt.server.solon.task; + +import org.dromara.mica.mqtt.server.solon.MqttServerTemplate; +import org.dromara.mica.mqtt.server.solon.pojo.User; +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 PublishTask { + @Inject + private MqttServerTemplate mqttServerTemplate; + + @Scheduled(fixedDelay = 1000) + public void publish() { + mqttServerTemplate.publishAll("/test/123", "mica最牛皮".getBytes(StandardCharsets.UTF_8)); + mqttServerTemplate.publishAll("/test/object", User.newUser()); + } + +} diff --git a/example/mica-mqtt-server-solon-plugin-example/src/main/resources/app.yml b/example/mica-mqtt-server-solon-plugin-example/src/main/resources/app.yml new file mode 100644 index 0000000..086a123 --- /dev/null +++ b/example/mica-mqtt-server-solon-plugin-example/src/main/resources/app.yml @@ -0,0 +1,54 @@ +# solon 配置 +solon: + logging: + appender: + console: + level: INFO +# 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 diff --git a/example/mica-mqtt-server-spring-boot-example/README.md b/example/mica-mqtt-server-spring-boot-example/README.md new file mode 100644 index 0000000..9e80be2 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/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/example/mica-mqtt-server-spring-boot-example/pom.xml b/example/mica-mqtt-server-spring-boot-example/pom.xml new file mode 100644 index 0000000..6bcfa2d --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + mica-mqtt-server-spring-boot-example + + + org.dromara.mica-mqtt + example + ${revision} + + + + + 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 + + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + + diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/MqttServerApplication.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/MqttServerApplication.java new file mode 100644 index 0000000..998ff16 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/MqttServerApplication.java @@ -0,0 +1,21 @@ +package org.dromara.mica.mqtt.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * @author wsq + */ +@SpringBootApplication +@EnableScheduling +public class MqttServerApplication { + + /** + * 先启动本项目,再启动 mica-mqtt-client-spring-boot-example 进行测试 + */ + public static void main(String[] args) { + SpringApplication.run(MqttServerApplication.class, args); + } + +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttAuthHandler.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttAuthHandler.java new file mode 100644 index 0000000..338e406 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttAuthHandler.java @@ -0,0 +1,21 @@ +package org.dromara.mica.mqtt.server.auth; + +import org.dromara.mica.mqtt.core.server.auth.IMqttServerAuthHandler; +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 { + + @Override + public boolean authenticate(ChannelContext context, String uniqueId, String clientId, String username, String password) { + // 客户端认证逻辑实现 + return true; + } + +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttHttpAuthFilter.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttHttpAuthFilter.java new file mode 100644 index 0000000..8491659 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/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/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttSubscribeValidator.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttSubscribeValidator.java new file mode 100644 index 0000000..e640605 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/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/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttUniqueIdService.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/auth/MqttUniqueIdService.java new file mode 100644 index 0000000..347721b --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/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/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/controller/CarInfoController.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/controller/CarInfoController.java new file mode 100644 index 0000000..7cdc70d --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/controller/CarInfoController.java @@ -0,0 +1,26 @@ +package org.dromara.mica.mqtt.server.controller; + +import lombok.AllArgsConstructor; +import org.dromara.mica.mqtt.server.entity.CarInfo; +import org.dromara.mica.mqtt.server.service.ICarInfoService; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/car") +@AllArgsConstructor +public class CarInfoController { + + private final ICarInfoService carInfoService; + + @GetMapping("/list") + public List list() { + return carInfoService.list(); + } + + @PostMapping + public boolean save(@RequestBody CarInfo carInfo) { + return carInfoService.save(carInfo); + } +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/controller/ServerController.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/controller/ServerController.java new file mode 100644 index 0000000..0ee901d --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/controller/ServerController.java @@ -0,0 +1,25 @@ +package org.dromara.mica.mqtt.server.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.dromara.mica.mqtt.server.service.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; + +@Tag(name = "Mqtt::服务端") +@RequestMapping("/mqtt/server") +@RestController +public class ServerController { + @Autowired + private ServerService service; + + @Operation(summary = "publish") + @PostMapping("publish") + public boolean publish(@RequestBody String body) { + return service.publish(body); + } + +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/entity/CarInfo.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/entity/CarInfo.java new file mode 100644 index 0000000..b025212 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/entity/CarInfo.java @@ -0,0 +1,12 @@ +package org.dromara.mica.mqtt.server.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("car_info") +public class CarInfo { + private Long customerId; + private String plate; + private String enable; +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener1.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener1.java new file mode 100644 index 0000000..9a28958 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener1.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.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/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener2.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener2.java new file mode 100644 index 0000000..bf4e791 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttConnectStatusListener2.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.server.listener; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.mica.mqtt.core.server.event.IMqttConnectStatusListener; +import org.springframework.stereotype.Service; +import org.tio.core.ChannelContext; + +/** + * mqtt 连接状态 + * + * @author L.cm + */ +@Slf4j +@Service +public class MqttConnectStatusListener2 implements IMqttConnectStatusListener { + + @Override + public void online(ChannelContext context, String clientId, String username) { + log.info("Mqtt clientId:{} username:{} online.", clientId, username); + } + + @Override + public void offline(ChannelContext context, String clientId, String username, String reason) { + log.info("Mqtt clientId:{} username:{} offline reason:{}.", clientId, username, reason); + } +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttServerMessageListener1.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttServerMessageListener1.java new file mode 100644 index 0000000..4016286 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/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 +//@Service +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/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttServerMessageListener2.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttServerMessageListener2.java new file mode 100644 index 0000000..416d401 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/listener/MqttServerMessageListener2.java @@ -0,0 +1,55 @@ +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.server.pojo.User; +import org.dromara.mica.mqtt.core.annotation.MqttServerFunction; +import org.springframework.stereotype.Service; +import org.tio.core.ChannelContext; +import org.tio.core.Node; + +import java.util.Map; + +/** + * 消息监听器示例2,MqttServerFunction 注解订阅,注意:如果自行实现了 IMqttMessageListener,MqttServerFunction 注解就不生效了。 + * + * @author wsq + */ +@Slf4j +@Service +public class MqttServerMessageListener2 { + + /** + * MQTT消息处理函数 + * + * @param topic mqtt Topic + * @param user 订阅消息的负载内容,默认 json 序列化 + */ + @MqttServerFunction("/test/object") + public void func1(String topic, User user) { + log.info("topic:{} user:{}", topic, user); + } + + @MqttServerFunction("/test/client") + public void func2(String topic, byte[] message) { + log.info("topic:{} message:{}", topic, new String(message)); + } + + /** + * MQTT消息处理函数,匹配 mqtt Topic /test/+,如何需要匹配所以消息,请使用通配符 # + * + * @param context ChannelContext,可选参数 + * @param topic 实际接收到消息的主题名称,可选参数 + * @param topicVars topic 中的 ${xxxx} 变量解析(v2.5.4支持),可选参数,注意:类型必须为 Map + * @param publishMessage 完整的MQTT发布消息对象,包含消息头和负载,可选参数 + * @param message 消息负载内容,以字节数组形式提供,可选参数,也可支持对象形式,默认 json 序列化 + */ + @MqttServerFunction("/test/${xxxx}") + public void func3(ChannelContext context, String topic, Map topicVars, MqttPublishMessage publishMessage, byte[] message) { + // 获取客户端节点信息 + Node clientNode = context.getClientNode(); + // 记录接收到的MQTT消息信息 + log.info("clientNode:{} topic:{} topicVars:{} publishMessage:{} message:{}", clientNode, topic, topicVars, publishMessage, new String(message)); + } + +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/mapper/CarInfoMapper.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/mapper/CarInfoMapper.java new file mode 100644 index 0000000..feab25d --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/mapper/CarInfoMapper.java @@ -0,0 +1,9 @@ +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.CarInfo; + +@Mapper +public interface CarInfoMapper extends BaseMapper { +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/pojo/User.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/pojo/User.java new file mode 100644 index 0000000..ab94704 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/pojo/User.java @@ -0,0 +1,19 @@ +package org.dromara.mica.mqtt.server.pojo; + +import lombok.Data; + +@Data +public class User { + private String name; + private T girlfriend; + + public static User newUser(){ + User user1 = new User(); + user1.setName("name1"); + + User user2 = new User(); + user2.setName("name2"); + user2.setGirlfriend(user1); + return user2; + } +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/ICarInfoService.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/ICarInfoService.java new file mode 100644 index 0000000..6d0bd84 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/ICarInfoService.java @@ -0,0 +1,7 @@ +package org.dromara.mica.mqtt.server.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.dromara.mica.mqtt.server.entity.CarInfo; + +public interface ICarInfoService extends IService { +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/ServerService.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/ServerService.java new file mode 100644 index 0000000..d858ae5 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/ServerService.java @@ -0,0 +1,24 @@ +package org.dromara.mica.mqtt.server.service; + +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(String body) { + boolean result = server.publishAll("/test/123", body.getBytes(StandardCharsets.UTF_8)); + log.info("Mqtt publishAll result:{}", result); + return result; + } +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/impl/CarInfoServiceImpl.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/impl/CarInfoServiceImpl.java new file mode 100644 index 0000000..76ccd34 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/service/impl/CarInfoServiceImpl.java @@ -0,0 +1,11 @@ +package org.dromara.mica.mqtt.server.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.dromara.mica.mqtt.server.entity.CarInfo; +import org.dromara.mica.mqtt.server.mapper.CarInfoMapper; +import org.dromara.mica.mqtt.server.service.ICarInfoService; +import org.springframework.stereotype.Service; + +@Service +public class CarInfoServiceImpl extends ServiceImpl implements ICarInfoService { +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/task/PublishAllTask.java b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/task/PublishAllTask.java new file mode 100644 index 0000000..aba2f86 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/java/org/dromara/mica/mqtt/server/task/PublishAllTask.java @@ -0,0 +1,25 @@ +package org.dromara.mica.mqtt.server.task; + +import org.dromara.mica.mqtt.core.server.MqttServer; +import org.dromara.mica.mqtt.server.pojo.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; + +/** + * @author wsq + */ +@Service +public class PublishAllTask { + @Autowired + private MqttServer mqttServer; + + @Scheduled(fixedDelay = 1000) + public void run() { + mqttServer.publishAll("/test/123", "mica最牛皮".getBytes(StandardCharsets.UTF_8)); + mqttServer.publishAll("/test/object", User.newUser()); + } + +} diff --git a/example/mica-mqtt-server-spring-boot-example/src/main/resources/application.yml b/example/mica-mqtt-server-spring-boot-example/src/main/resources/application.yml new file mode 100644 index 0000000..d81f8a9 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/src/main/resources/application.yml @@ -0,0 +1,104 @@ +server: + port: 30013 +spring: + application: + name: mica-mqtt-server + + datasource: + url: jdbc:mysql://127.0.0.1:3306/xa_cloud?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + type: com.alibaba.druid.pool.DruidDataSource + + #mybatis + mybatis-plus: + mapper-locations: classpath:/mapper/**/*.xml + #实体扫描,多个package用逗号或者分号分隔 + typeAliasesPackage: org.dromara.mica.mqtt.server.entity + global-config: + #数据库相关配置 + db-config: + #主键类型 + id-type: ASSIGN_ID + banner: false + #原生配置 + configuration: + map-underscore-to-camel-case: 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: 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 +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/example/mica-mqtt-server-spring-boot-example/src/main/resources/banner.txt b/example/mica-mqtt-server-spring-boot-example/src/main/resources/banner.txt new file mode 100644 index 0000000..2eb7254 --- /dev/null +++ b/example/mica-mqtt-server-spring-boot-example/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/example/pom.xml b/example/pom.xml new file mode 100644 index 0000000..4ff4dde --- /dev/null +++ b/example/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + org.dromara.mica-mqtt + mica-mqtt + ${revision} + + example + ${project.artifactId} + pom + + + + 17 + + 4.0.0 + + 4.0.0 + + 2.13.2 + + + + mica-mqtt-example + mica-mqtt-client-spring-boot-example + mica-mqtt-server-spring-boot-example + mica-mqtt-client-solon-plugin-example + mica-mqtt-server-solon-plugin-example + + + + + + + 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 + + + + + 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..3aa59e6 --- /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 + example + + + + 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 + + + + +