Spring boot 開發 MQTT 物聯網消息服務(kotlin)

前言

項目中有針對自己公司研發的終端設備需要遠程控制和遠程參數設置的功能,還需要將設備上報的狀態數據監控到,在技術選型上,一般這種情況都選擇Mina 或者 Netty 做長鏈接。
但是長鏈接也有問題針對於協議設計和安全性上需要重新設計和考慮。
所以在技術選型上直接按照現有協議來選擇,比較完善的及時通信協議有MQTT、XMPP、JMS等。
最終因爲MQTT協議低耦合、輕量級的特點,選擇成爲設備遠程控制協議。

MQTT

MQTT(Message Queuing Telemetry Transport,消息隊列遙測傳輸協議),是一種基於發佈/訂閱(publish/subscribe)模式的"輕量級"通訊協議,該協議構建於TCP/IP協議上,由IBM在1999年發佈。MQTT最大優點在於,可以以極少的代碼和有限的帶寬,爲連接遠程設備提供實時可靠的消息服務。作爲一種低開銷、低帶寬佔用的即時通訊協議,使其在物聯網、小型設備、移動應用等方面有較廣泛的應用。

MQTT是一個基於客戶端-服務器的消息發佈/訂閱傳輸協議。MQTT協議是輕量、簡單、開放和易於實現的,這些特點使它適用範圍非常廣泛。在很多情況下,包括受限的環境中,如:機器與機器(M2M)通信和物聯網(IoT)。其在,通過衛星鏈路通信傳感器、偶爾撥號的醫療設備、智能家居、及一些小型化設備中已廣泛使用。

引用自菜鳥教程 MQTT 內容 https://www.runoob.com/w3cnote/mqtt-intro.html

MQTT 實現

協議終歸只是規範一個過程,但是基於規範肯定是要實現的。
MQTT 既然是一個成熟協議,那麼實現該協議的中間件也就會有很多,我們列舉出常用的兩個實現,供各位選擇。

Eclipse Mosquitto

看名字就知道是Java世界有名的Eclipse 基金會下的一個MQTT 協議的通信中間件。C語言開發的,性能和穩定性沒得說。
配置不在這邊贅述,需要學習的話,建議參考其他博主的教程,或者去官網學習。

官方地址 https://mosquitto.org/

Apache Apollo

Apollo也出身豪門Apache 基金會,但是Apollo項目其實並不能單純的算MQTT協議的中間件,其代碼是在ActiveMQ基礎上發展而來的,可以支持STOMP, AMQP, MQTT, Openwire, SSL, WebSockets 等多種通信協議。

官網地址 https://activemq.apache.org/

其實僅需要MQTT通信的話,Eclipse Mosquitto 就夠用了,再加上其實Springboot 中對MQTT支持其實用的是Eclipse Paho 作爲客戶端程序的。所以最終選擇在服務器上部署Eclipse Mosquitto 作爲消息中間件。

Spring Boot Integration MQTT

gradle 依賴

dependencies {

    implementation "org.springframework.boot:spring-boot-starter-integration"
    implementation "org.springframework.integration:spring-integration-stream"
    implementation "org.springframework.integration:spring-integration-mqtt"
    
    }

從依賴就可以看出,其實spring對標準協議這塊的支持模塊化做的很優秀的。

MQTT Config

import org.eclipse.paho.client.mqttv3.MqttConnectOptions
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.integration.annotation.ServiceActivator
import org.springframework.integration.channel.DirectChannel
import org.springframework.integration.core.MessageProducer
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory
import org.springframework.integration.mqtt.core.MqttPahoClientFactory
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter
import org.springframework.messaging.MessageChannel
import org.springframework.messaging.MessageHandler
import org.unreal.hardware.receiver.handler.MessageReceiverHandler


@Configuration
class MqttConfig {

    /* @Value("\${spring.mqtt.username}")
     private val username: String? = null

     @Value("\${spring.mqtt.password}")
     private val password: String? = null*/

    @Value("\${spring.mqtt.url}")
    private lateinit var url: String

    @Value("\${spring.mqtt.producer.clientId}")
    private lateinit var producerClientId: String

    @Value("\${spring.mqtt.producer.defaultTopic}")
    private lateinit var producerDefaultTopic: String

    @Value("\${spring.mqtt.consumer.clientId}")
    private lateinit var consumerClientId: String

    @Value("\${spring.mqtt.consumer.deviceStatusTopic}")
    private lateinit var consumerDeviceStatusTopic: String

    @Value("\${spring.mqtt.timeout}")
    private lateinit var timeout: String

    @Value("\${spring.mqtt.keepalive}")
    private lateinit var keepalive: String

    @Autowired
    private lateinit var messageReceiverHandler: MessageReceiverHandler


    private val logger = LoggerFactory.getLogger(MqttConfig::class.java)

    /**
     * MQTT連接器選項
     *
     * @return [org.eclipse.paho.client.mqttv3.MqttConnectOptions]
     */
    // 設置是否清空session,這裏如果設置爲false表示服務器會保留客戶端的連接記錄,
    // 這裏設置爲true表示每次連接到服務器都以新的身份連接
    // 設置連接的用戶名 千萬不能重複,一旦重複就會出現 ,新連接就會出現擠掉同Id的舊連接
    // 設置連接的密碼
    // 設置超時時間 單位爲秒
    // 設置會話心跳時間 單位爲秒 服務器會每隔1.5*20秒的時間向客戶端發送心跳判斷客戶端是否在線,但這個方法並沒有重連的機制
    // 設置“遺囑”消息的話題,若客戶端與服務器之間的連接意外中斷,服務器將發佈客戶端的“遺囑”消息。
    val mqttConnectOptions: MqttConnectOptions
        @Bean
        get() {
            val options = MqttConnectOptions()
            options.isCleanSession = true
            /*options.userName = username
            options.password = password!!.toCharArray()*/
            options.serverURIs = arrayOf(url)
            options.connectionTimeout = timeout.toInt()
            options.keepAliveInterval = keepalive.toInt()
            options.setWill("/server/offline", WILL_DATA, 2, false)
            return options
        }


    /**
     * MQTT客戶端
     *
     * @return [org.springframework.integration.mqtt.core.MqttPahoClientFactory]
     */
    @Bean
    fun mqttClientFactory(): MqttPahoClientFactory {
        val factory = DefaultMqttPahoClientFactory()
        factory.connectionOptions = mqttConnectOptions
        return factory
    }

    /**
     * MQTT信息通道(生產者)
     *
     * @return [org.springframework.messaging.MessageChannel]
     */
    @Bean(name = [CHANNEL_NAME_OUT])
    fun mqttOutboundChannel(): MessageChannel {
        return DirectChannel()
    }

    /**
     * MQTT消息處理器(生產者)
     *
     * @return [org.springframework.messaging.MessageHandler]
     */
    @Bean
    @ServiceActivator(inputChannel = CHANNEL_NAME_OUT)
    fun mqttOutbound(): MessageHandler {
        val messageHandler = MqttPahoMessageHandler(
            producerClientId,
            mqttClientFactory()
        )
        messageHandler.setAsync(true)
        messageHandler.setDefaultTopic(producerDefaultTopic)
        return messageHandler
    }

    /**
     * MQTT消息訂閱綁定(消費者)
     *
     * @return [org.springframework.integration.core.MessageProducer]
     */
    @Bean
    fun inbound(): MessageProducer {
        // 可以同時消費(訂閱)多個Topic
        val adapter = MqttPahoMessageDrivenChannelAdapter(
            consumerClientId, mqttClientFactory(),
            consumerDeviceStatusTopic
        )
        adapter.setCompletionTimeout(5000)
        adapter.setConverter(DefaultPahoMessageConverter())
        adapter.setQos(1)
        // 設置訂閱通道
        adapter.outputChannel = mqttInboundChannel()
        return adapter
    }

    /**
     * MQTT信息通道(消費者)
     *
     * @return [org.springframework.messaging.MessageChannel]
     */
    @Bean(name = [CHANNEL_NAME_IN])
    fun mqttInboundChannel(): MessageChannel {
        return DirectChannel()
    }

    /**
     * MQTT消息處理器(消費者)
     * 自行實現messageHandler 即可接收到訂閱路徑發上來的消息
     * @return [org.springframework.messaging.MessageHandler]
     */
    @Bean
    @ServiceActivator(inputChannel = CHANNEL_NAME_IN)
    fun handler(): MessageHandler {
        return messageReceiverHandler
    }

    companion object {


        private val WILL_DATA: ByteArray = "offline".toByteArray()

        /**
         * 訂閱的bean名稱
         */
        const val CHANNEL_NAME_IN = "mqttInputChannel"
        /**
         * 發佈的bean名稱
         */
        const val CHANNEL_NAME_OUT = "mqttOutboundChannel"

    }
}

可以看到上面的一些參數實際上是在application.yml中配置的,所以也貼出application.yml文件中的配置

spring:
  mqtt:
    url: tcp://192.168.0.9:1883
    timeout: 10
    keepalive: 20
    producer:
      clientId: hardwareServerSender
      defaultTopic: device
    consumer:
      clientId: hardwareServerReceiver
      deviceStatusTopic: /server/device/status

以上 Mqtt 環境配置完成。

發送消息

@Component
@MessagingGateway(defaultRequestChannel = MqttConfig.CHANNEL_NAME_OUT)
interface MqttSender {

    /**
     * 發送信息到MQTT服務器
     *
     * @param data 發送的文本
     */
    fun sendToMqtt(data: String)

    /**
     * 發送信息到MQTT服務器
     *
     * @param topic 主題
     * @param payload 消息主體
     */
    fun sendToMqtt(
        @Header(MqttHeaders.TOPIC) topic: String,
        payload: String
    )

    /**
     * 發送信息到MQTT服務器
     *
     * @param topic 主題
     * @param qos 對消息處理的幾種機制。<br></br> 0 表示的是訂閱者沒收到消息不會再次發送,消息會丟失。<br></br>
     * 1 表示的是會嘗試重試,一直到接收到消息,但這種情況可能導致訂閱者收到多次重複消息。<br></br>
     * 2 多了一次去重的動作,確保訂閱者收到的消息有一次。
     * @param payload 消息主體
     */
    fun sendToMqtt(
        @Header(MqttHeaders.TOPIC) topic: String,
        @Header(MqttHeaders.QOS) qos: Int,
        payload: String
    )
}

調用發送時,僅需要將MqttSender的實例化對象注入到需要的地方,調用對應方法即可完成消息發送.
SAMPLE:

@Service
class DeviceCommandServiceImpl : DeviceCommandService {


    @Autowired
    private lateinit var mqttSender: MqttSender

    fun openGate(sn: String) {
        sendMQTTMessage(sn, "(option:\"openGate\",time:\""+System.currentTime()+"\"}")
    }
}

訂閱消息

在配置的時候,其實將消息處理的Handler 已經配置在MQTT中,所以僅需要實現消息處理的hander即可。

@Service
class MessageReceiverHandler : MessageHandler {

    @Autowired
    private lateinit var deviceStatusMessageProcessor: DeviceStatusMessageProcessor

    @Autowired
    private lateinit var deviceCommandMessageProcessor: DeviceCommandMessageProcessor

    @Autowired
    private lateinit var deviceParamsMessageProcessor: DeviceParamsMessageProcessor

    @Autowired
    private lateinit var deviceNamelistMessageProcessor: DeviceNamelistMessageProcessor

    @Autowired
    private lateinit var devicePrinterMessageProcessor: DevicePrinterMessageProcessor

    private val logger = LoggerFactory.getLogger(MessageReceiverHandler::class.java)

    private val statusTopic = "status"

    override fun handleMessage(message: Message<*>) {
        val topic = message.headers["mqtt_receivedTopic"].toString()
        val type = topic.substring(topic.lastIndexOf("/") + 1, topic.length)
        logger.debug("MQTT handler receiver message type is ------> $type")
        logger.debug("MQTT handler receiver message is ------> ${message.payload}"
        TODO("在此處完成業務代碼即可")
    }
}

總結

以上就完成了對MQTT協議的對接處理。其實主要就是發送 和接收 處理好這兩部分就可以了。
代碼不建議複製粘貼,自己動手寫一下對代碼的理解會更好。有問題可以通過站內信聯繫我,看到了一定會回覆的!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章