谈谈对 MQTT 和 WebSocket 的理解

1,795 阅读6分钟

MQTT

MQTT 是一种轻量级的通信协议,通常用于物联网设备之间的通讯。它基于发布/订阅模式,允许客户端订阅特定主题并接收与该主题相关的消息,MQTT 协议简单高效,适用于低带宽环境下的通讯需求。

添加依赖

        maven { url "https://repo.eclipse.org/content/repositories/paho-snapshots/" }
    implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
    implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'

首先,连接服务器,SERVER_HOST 为主机名,CLIENT_ID 为客户端唯一标识,还需要用户名和密码,当然,这些一般由服务器返回。

    fun connect() {
        try {
            // MemoryPersistence 设置 clientId 的保存形式,默认为以内存保存
            client = MqttAsyncClient(SERVER_HOST, CLIENT_ID, MemoryPersistence())
            // MQTT 连接设置
            options = MqttConnectOptions()
            with(options) {
                //是否清空 session,true 表示每次连接到服务器都以新的身份,false 表示服务器会保留客户的连接记录
                isCleanSession = true
                // 用户名和密码
                userName = USERNAME
                password = PASSWORD.toCharArray()
                // 超时时间,单位是秒
                connectionTimeout = 30
                // 心跳时间,单位是秒
                keepAliveInterval = 30
            }
            //设置回调
            client!!.setCallback(PushCallBack())
            client!!.connect(options, context, iMqttActionListener)
        } catch (e: MqttException) {
            e.printStackTrace()
        }
    }

心跳包:在空闲时间内,如果客户端没有其他任何报文发送,必须发送一个 PINGREQ 报文到服务器,而如果服务端在 1.5 倍心跳时间内没有收到客户端消息,则会主动断开客户端的连接,发送其遗嘱消息给所有订阅者,而服务端收到 PINGREQ 报文之后,立即返回 PINGRESP 报文给客户端。

如果两个客户端的 clientID 一样,那么服务端记录第一个客户端连接之后再收到第二个客户端连接请求,则会向第一个客户端发送 Disconnect 报文断开连接,并连接第二个客户端。

遗嘱消息:MqttConnectOptions 的 setWill 方法可以设置遗嘱消息,客户端没有主动向服务端发起 disconnect 断开连接消息,但服务端检测到和客户端的连接已断开,此时服务端将该客户端设置的遗嘱消息发送出去,遗嘱 Topic 中不能存在通配符。 应用场景:客户端掉线之后,可以及时通知到所有订阅该遗嘱 topic 的客户端。

订阅消息。

    fun subscribeMessage(topic: String, qos: Int) {
        client?.let {
            try {
                it.subscribe(topic, qos)
            } catch (e: MqttException) {
                e.printStackTrace()
            }
        }
    }

MQTT 是一种发布/订阅的消息协议, 通过设定的主题 topic,发布者向 topic 发送的 payload 负载消息会经过服务器,转发到所有订阅该 topic 的订阅者。topic 有两个通配符,“+” 和 “#”,与 “/” 组合使用,“+” 只能表示一级 topic,“#” 可以表示任意层级,例如,订阅 topic 为 “guangdong/+”,发布者发布的 topic 可以是 guangdong,guangdong/shenzhen,但是不能是 guangdong/shenzhen/nanshan,而订阅的 topic 如果是 “guangdong/#” 则可以收到。

Qos 为服务质量等级:

  • qos = 0,表示发送方只发一次,不管是否发成功,也不管接收方是否成功接收,适用于不重要的数据传输。
  • qos = 1,表示消息至少有一次到达接收方,发送方发送消息,需要等待接收方返回应答消息,如果发送方在一定时间内未收到应答,发送方将继续发送,直到收到应答消息,删除本地消息缓存,不再发送。适用于需要收到所有消息,客户端可以处理重复消息。
  • qos = 2:确保消息只一次到达接收方,一般我们都会选择这个。

发布消息

    fun publish(topic: String, msg: String, isRetained: Boolean, qos: Int) {
        try {
            client?.let {
                val message = MqttMessage()
                message.qos = qos
                message.isRetained = isRetained
                message.payload = msg.toByteArray()
                it.publish(topic, message)
            }
        } catch (e: MqttPersistenceException) {
            e.printStackTrace()
        } catch (e: MqttException) {
            e.printStackTrace()
        }
    }

payload 为负载消息,字节流类型,是 MQTT 通信传输的真实数据。retain 是保留信息,服务端将保留对应 topic 最新的一条消息记录,保留消息的作用是每次客户端连上都会收到其 topic 的最后一条保留消息。

整个封装类如下:

class MQTTManager private constructor(private val context: Context) {

    private var client: MqttAsyncClient? = null
    private lateinit var options: MqttConnectOptions

    private val iMqttActionListener = object : IMqttActionListener {
        override fun onSuccess(asyncActionToken: IMqttToken?) {
            //连接成功,开始订阅
            subscribeMessage(TOPIC, 2)
        }

        override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
            //连接失败
        }
    }

    companion object {
        @Volatile
        private var instance: MQTTManager? = null

        fun getInstance(context: Context): MQTTManager = instance ?: synchronized(this) {
            instance ?: MQTTManager(context).also {
                instance = it
            }
        }
    }

    /**
     * 连接服务器
     */
    fun connect() {
        try {
            // MemoryPersistence 设置 clientId 的保存形式,默认为以内存保存
            client = MqttAsyncClient(SERVER_HOST, CLIENT_ID, MemoryPersistence())
            // MQTT 连接设置
            options = MqttConnectOptions()
            with(options) {
                // 是否清空 session,true 表示每次连接到服务器都以新的身份,false 表示服务器会保留客户的连接记录
                isCleanSession = true
                // 用户名和密码
                userName = USERNAME
                password = PASSWORD.toCharArray()
                // 超时时间,单位是秒
                connectionTimeout = 30
                // 心跳时间,单位是秒
                keepAliveInterval = 30
                // 自动重连
                isAutomaticReconnect = true
            }
            // 设置回调
            client!!.setCallback(PushCallBack())
            client!!.connect(options, context, iMqttActionListener)
        } catch (e: MqttException) {
            e.printStackTrace()
        }
    }

    /**
     * 订阅消息
     */
    fun subscribeMessage(topic: String, qos: Int) {
        client?.let {
            try {
                it.subscribe(topic, qos)
            } catch (e: MqttException) {
                e.printStackTrace()
            }
        }
    }

    /**
     * 发布消息
     */
    fun publish(topic: String, msg: String, isRetained: Boolean, qos: Int) {
        try {
            client?.let {
                val message = MqttMessage()
                message.qos = qos
                message.isRetained = isRetained
                message.payload = msg.toByteArray()
                it.publish(topic, message)
            }
        } catch (e: MqttPersistenceException) {
            e.printStackTrace()
        } catch (e: MqttException) {
            e.printStackTrace()
        }
    }

    /**
     * 断开连接
     */
    fun disconnect() {
        client?.takeIf {
            it.isConnected
        }?.let {
            try {
                it.disconnect()
                instance = null
            } catch (e: MqttException) {
                e.printStackTrace()
            }
        }
    }

    /**
     * 判断是否连接
     */
    fun isConnected() = if (client != null) client!!.isConnected else false

    inner class PushCallBack : MqttCallback {
        override fun connectionLost(cause: Throwable?) {
            // 断开连接
        }

        override fun messageArrived(topic: String?, message: MqttMessage?) {
            // 接收消息回调
        }

        override fun deliveryComplete(token: IMqttDeliveryToken?) {
            // 发布消息完成后的回调
        }
    }

}

WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它允许客户端和服务器之间进行实时,低延迟的双向通信。WebSocket 建立连接后可以保持长时间的连接状态,从而使得服务器可以主动向客户端推送数据,WebSocket 协议通常用于实现实时聊天、在线游戏、股票行情推送等需要实时性的网络应用。

这里使用 OKHTTP 进行 WebSocket 开发。

class WebSocketManager private constructor() {

    private val client: OkHttpClient = OkHttpClient.Builder().writeTimeout(5, TimeUnit.SECONDS)
        .readTimeout(5, TimeUnit.SECONDS)
        .connectTimeout(5, TimeUnit.SECONDS)
        .build()

    private var webSocket: WebSocket? = null

    companion object {
        val instance: WebSocketManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { WebSocketManager() }
    }

    fun connect(url: String) {
        webSocket?.cancel()
        val request = Request.Builder().url(url).build()
        webSocket = client.newWebSocket(request, object : WebSocketListener() {

            // 连接成功后回调
            override fun onOpen(webSocket: WebSocket, response: Response) {
                super.onOpen(webSocket, response)
            }

            // 服务器发送消息给客户端时回调
            override fun onMessage(webSocket: WebSocket, text: String) {
                super.onMessage(webSocket, text)
            }

            // 服务器发送的 byte 类型消息
            override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
                super.onMessage(webSocket, bytes)
            }

            // 服务器连接关闭中
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                super.onClosing(webSocket, code, reason)
            }

            // 服务器连接已关闭
            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                super.onClosed(webSocket, code, reason)
            }

            // 服务器连接失败
            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                super.onFailure(webSocket, t, response)
            }
        })
    }

    // 发送消息
    private fun sendMessage(message: String) {
        webSocket?.send(message)
    }

    private fun close(code: Int, reason: String) {
        webSocket?.close(code, reason)
    }

}

总结

MQTT 和 WebSocket 是两种不同的通信协议,它们之间的主要区别包括以下几点:

  • 通信模式:MQTT 基于发布/订阅模式,客户端可以订阅感兴趣的主题,并接收相关消息。WebSocket 为全双工通信,客户端和服务器之间可以实现双向实时通信。

  • 协议特点:MQTT 轻量高效,适合在资源有限的设备上使用,具有断线重连机制和 QoS 等级控制。WebSocket 是基于 HTTP 协议升级而来,可在单个 TCP 连接上进行全双工通信,适合实时性要求较高的应用。

  • 连接方式:MQTT 建立在 TCP 连接之上,在连接建立后保持长连接状态。WebSocket 也是建立在 TCP 连接之上,但与 HTTP 不同的是,WebSocket 连接是全双工的,可以保持长时间连接状态。

  • 应用场景: MQTT 主要用于物联网设备之间的通讯,适合在低带宽,不稳定网络环境下使用,例如传感器数据采集、实时监控等场景。WebSocket 适用于需要实时双向通信的场景,如在线聊天、实时数据推送等。