1. Client 主动关闭连接

Client 主动关闭连接的流程非常简单,只需要向 Broker 发送一个 DISCONNECT 数据包就可以了。
DISCONNECT 数据包没有可变头(Variable header)和消息体(Payload)。在 Client 发送完DISCONNECT 之后,就可以关闭底层的 TCP 连接了,不需要等待 Broker 的回复(Broker 也不会对DISCONNECT 数据包回复)。

为什么需要在关闭 TCP 连接之前,发送一个和 Broker 没有交互的 DISCONNECT数据包,而不是直接关闭底层的 TCP 连接?

这里涉及到 MQTT 协议的一个特性,Broker 需要判断 Client 是否正常地断开连接。

当 Broker 收到 Client 的 DISCONNECT 数据包的时候,它认为 Client 是正常地断开连接,那么它会丢弃当前连接指定的遗愿消息(Will Message)。如果 Broker 检测到 Client 连接丢失,但又没有收到DISCONNECT 消息包,它会认为 Client 是非正常断开连接,就会向在连接的时候指定的遗愿主题(Will Topic)发布遗愿消息(Will Message)。

2. Broker 主动关闭连接

MQTT 协议规定 Broker 在没有收到 Client 的 DISCONNECT 数据包之前都应该保持和 Client 连接,只有Broker 在 Keep Alive 的时间间隔里,没有收到 Client 的任何 MQTT 数据包的时候会主动关闭连接。一些Broker 的实现在 MQTT 协议上做了一些拓展,支持 Client 的连接管理,可以主动地断开和某个 Client 的连接。
Broker 主动关闭连接之前不会向 Client 发送任何 MQTT 数据包,直接关闭底层的 TCP 连接就完事了。

3. 代码实践

接下来使用代码来展示 MQTT 连接的建立,和断开各种情况下的示例。
案例是在ubuntu下安装,使用node.js演示

  1. # 1.安装mqtt broker (及其搭配的客户端)
  2. # 这里使用的是mosquitto,可以参考 《MQTT 的基础概念》一文找到自己想要的服务
  3. apt-get install mosquitto
  4. apt-get install mosquitto-clients
  5. # 2. 安装Node.js 的 MQTT 库(请先确保安装node,可以通过nvm来安装方便)
  6. npm install mqtt --save

3.1 建立持久会话的连接

创建 persistent_connection.js 如下:

  1. //引用 MQTT 库
  2. var mqtt = require('mqtt')
  3. //建立连接
  4. var client = mqtt.connect('mqtt://10.10.20.200:1883', {
  5. clientId: "mqtt_sample_id_1",
  6. clean: false
  7. })
  8. client.on('connect', function (connack) {
  9. console.log(`return code: ${connack.returnCode}, sessionPresent: ${connack.sessionPresent}`)
  10. client.end()
  11. })

通过 ClientID 选项指定 Client Identifier,并通过 Clean 选项设定 Clean Session 为 false,代表我们要建立一个持久化会话的连接。
最后通过捕获 connect 事件将 CONNACK 包 Return Code 和 Session Present Flag 打印出来,然后断开连接。

在终端上运行:

  1. node persistent_connection.js

输出:

  1. return code: 0, sessionPresent: false

连接成功,因为是“mqtt_sample_id_1”的 Client 第一次建立连接,所以 SessionPresent 为 false。
再次运行node persistent_connection.js,输出就会变成:

  1. return code: 0, sessionPresent: true

3.2 建立非持久会话的连接

上面代码只需要将 Clean 选项设为 true,就可以建立一个非持久会话的连接了。
终端运 node persistent_connection.js 最终输出 return code: 0, sessionPresent: false 无论运行多少次, SessionPresent 都将为 false。

3.3 使用相同的 Client Identifier 进行连接

如果两个 Client 使用同样的 Client Identifier 会发生什么。把代码稍微调整下,在连接成功的时候保持连接,然后捕获 offline 事件,在 Client 的连接被关闭的时候打印出来。
创建 identifcal.js 如下:

  1. var mqtt = require('mqtt')
  2. var client = mqtt.connect('mqtt://10.10.20.200:1883', {
  3. clientId: "mqtt_identical_1",
  4. })
  5. client.on('connect', function (connack) {
  6. console.log(`return code: ${connack.returnCode}, sessionPresent: ${connack.sessionPresent}`)
  7. })
  8. client.on('offline', function () {
  9. console.log("client went offline")
  10. })

然后我们打开两个终端,分别在上面运行 node identifcal.js,然后我们会看到在两个终端上不停地出现以下打印:

  1. return code: 0, sessionPresent: false
  2. client went offline
  3. return code: 0, sessionPresent: false
  4. client went offline
  5. return code: 0, sessionPresent: false
  6. .......

在 MQTT 中,在两个 Client 使用相同的 Client Identifier 进行连接时,如果第二个 Client 连接成功,Broker会关闭和第一个已经连接上的 Client 连接。由于我们使用的 MQTT 库实现了断线重连的功能,所以当连接被Broker 关闭时,它又会尝试重新连接,结果就是这两个 Client 交替地把对方顶下线,我们就会看到这样的打印输出。因此在实际应用中,一定要保证每一个设备使用的 Client Identifier 是唯一的。
如果你观察到一个 Client 不停地上线下线,那么有很大可能是 Client Identifier 冲突的问题。