MQTT协议有部分是基于webSocket进行封装的,也许在其他端有差异(因为MQTT设计之初是为物联网服务的--好像),但是在web端就是如此(好像还有TCP的链接方式但是我没有用,所以不太了解),其源码的建立连接部分也是使用了new WebSocket

传统的 websocket 是直接和服务器沟通的,只要连上了,web 就可以接收到服务器主动发起的数据,而 MQTT 并不是直接和服务器沟通,或者说也算服务器,但是被称作 MQTT Broker 的中介服务,简单的理解就是我们在创建链接的时候,还需要订阅信息--所谓订阅,类似于我们在关注了某个UP主,当这个UP主有了新的视频更新的时候,你就能接受到消息。我们订阅了某个Topic之后,如果此时这个Topic(项目中可能是服务器,也可能是另外一个 web client 或其他端)或告诉 MQTT Broker,我发布了一条消息, 那中介 Broker 就会告诉所有订阅了这个 Topic 的 client ,你关注的UP主发视频了,快去看啊。如此,就完成了一个消息的互通和传递流程。

RabbitMQ 简单介绍

RabbitMQ is the most widely deployed open source message broker. 这是官网的原文,意思是RabbitMQ是部署最广泛的开源消息代理,也就是我们上面讲的消息中转中心(中介)。通过在本地部署服务,可以快速的构建一个Broker,对于我们开发者来说,就是可以快速的搭建服务器,用来测试我们的传输协议和订阅发布有没有正常联通。

项目中的实现

下面就是本次文章的最终结果,具体的代码,还有一些注意事项:

前端 - React

import mqtt from 'mqtt' // mqtt@5.2.1
import { useEffect, useRef } from 'react' // react@18.2.0
import axios from 'axios'

// 定义一个hooks,此处的cb是为了在接收到消息后可以在外部定义函数处理返回值
export const useMQTT = (cb) => {
  // 项目原因,需要获取的id
  const STF_UID = sessionStorage.getItem('STF_UID')
  // 传入mqtt的options 数据
  const mqttOptions = {
    // 保活周期,是一个以秒为单位的时间间隔。客户端在无报文发送时,
    // 将按 Keep Alive 设定的值定时向服务端发送心跳报文,确保连接不被服务端断开。
    keepalive: 30,
    // 协议版本,使用较多的 MQTT 协议版本有 MQTT v3.1、MQTT v3.1.1 及 MQTT v5.0。
    protocolVersion: 4,
    // 为 false 时表示创建一个持久会话,在客户端断开连接时,会话仍然保持并保存离线消息,直到会话超时注销。
    // 为 true 时表示创建一个新的临时会话,在客户端断开时,会话自动销毁。
    // 持久会话恢复的前提是客户端使用固定的 Client ID 再次连接,
    // 如果 Client ID 是动态的,那么连接成功后将会创建一个新的持久会话。
    clean: true,
    // 重连周期
    reconnectPeriod: 5000,
    // 连接超时时长,收到服务器连接确认前的等待时间,等待时间内未收到连接确认则为连接失败。
    connectTimeout: 30 * 1000,
    // 遗嘱消息是 MQTT 为那些可能出现意外断线的设备提供的将遗嘱优雅地发送给其他客户端的能力。
    will: {
      // 当设备意外断线时,遗嘱消息将被发送至遗嘱 Topic
      topic: 'WillMsg',
      // 待发送的消息内容;
      payload: 'Connection Closed abnormally..!',
      // QoS 0 - 最多交付一次
      // QoS 1 - 至少交付一次
      // QoS 2 - 只交付一次
      qos: 0,
      retain: false,
    },
    // 拒绝非授权用户
    rejectUnauthorized: false,
    // MQTT 服务器使用 Client ID 识别客户端,连接到服务器的每个客户端都必须要有唯一的 Client ID
    clientId: 'mqtt_fms_' + STF_UID + Math.random().toString(16).substr(2, 8),
    // MQTT 协议可以通过用户名和密码来进行相关的认证和授权,
    // 但是如果此信息未加密,则用户名和密码将以明文方式传输。
    // 如果设置了用户名与密码认证,那么最好要使用 mqtts 或 wss 协议。
    username: 'admin',
    password: 'admin',
  }
  // 定义一个ref存储 用户的client
  const client = useRef()

  // 项目要求,登录用户才可以发起
  const token = sessionStorage.getItem('token')

  // 定义一个链接mqtt的函数,传入要订阅的topic
  const connectMqtt = async (topic) => {
    // 获取本地config的数据
    const time = new Date().getTime()
    axios.get(window.location.origin + '/config.json?time=' + time).then((res) => {
      // 这里的url是根据服务进行确定,比如我链接我本地的RabbitMQ,
      // url = web-mqtt://localhost:15675/ws
      // 如果是想链接mqtt官网提供的测试地址就填写ws://broker.emqx.io:8083/mqtt
      // 但是官网的测试地址想要测试具体的,需要下载MQTTX软件一起
      const url = res?.data?.MQTT
      // 如果有url,就进行链接
      if (url) {
        // 发起链接--到这一步,如果成功了,其实基本也就成功了
        client.current = mqtt.connect(url, mqttOptions)
        // 直接发起订阅--如果没有发起成功,并且已经链接上了,
        // 可以检查一下用户密码是不是有问题
        client.current?.subscribe(topic, 0, (err) => {
          if (err) {
            console.log('subscribe error')
          } else {
            console.log(topic, 'subscribe')
          }
        })

        // 发起订阅后,如果broker有消息传输过来,在这里接收
        client.current?.on('message', (topicR, message, packet) => {
          if (packet.length > 0) {
            cb(topicR, JSON.parse(message))
          }
        })
      }
    })
  }

  // 加载时触发
  useEffect(() => {
    if (token) {
      // 如果client 已经存在了
      if (client?.current) {
        // 就直接发起链接
        // https://github.com/mqttjs/MQTT.js#event-connect
        client?.current.on('connect', () => {
          // console.log('connection successful')
        })

        // https://github.com/mqttjs/MQTT.js#event-error
        client?.current.on('error', (err) => {
          console.error('Connection error: ', err)
          client?.current.end()
        })

        // https://github.com/mqttjs/MQTT.js#event-reconnect
        client?.current.on('reconnect', () => {})
      } else {
        // 如果 client 还不存在,就走创建的流程
        connectMqtt('fms_user_notification_' + STF_UID)
      }
    }
  }, [])

  return { client, connectMqtt }
}

rabbitMq 部分

这部分主要是服务器的搭建和mqtt插件的引入

  1. 安装 Erlang - 因为 RabbitMQ 是基于这个开发的
  2. 下载并安装 RabbitMQ ,下载地址 (就按照官网的走就好了,提供了不同系统的不同版本,我使用的是window版本)
  3. 安装完成之后,他会提供一些快捷开启和关闭服务的图标,如果需要对接mqtt就先不要直接启动。需要进入到安装地址下的sbin文件夹下,比如我的是安装在D盘的(D:\00_software\RabbitMQ\rabbitmq_server-3.12.8\sbin)路径下, 根据个人的地址进入相应路径。
  4. 启动管理员的cmd,开启mqtt相关的插件以及网页端服务管理的插件
开启网页管理服务,可以直接在网页上管理服务--默认的账号密码都时guest
rabbitmq-plugins enable rabbitmq_management
启动mqtt broker服务
rabbitmq-plugins enable rabbitmq_web_mqtt
  1. 开启服务,http://localhost:15672/ 地址下面可以进入网页管理

数据互通

在两者都准备好之后,就差一个联通功能了,其实前面的前端代码里面已经写了,就是根据启动的Broker地址,设定一个url,然后前端调用mqtt.connect 链接就可以了。

碰到的一些问题,和一些意外的收获

安装RabbitMQ后无法链接上

  1. 检查是不是没有开启对应的插件
  2. 检查端口是否正确
  3. 新开启的插件是不会直接应用的,需要先关闭服务,然后重新启动服务
  4. 这里的关闭和开启不能走软件提供的快捷方式,需要在命令行输入,不然可能会不生效--不知道为啥

连接上了但是订阅失败

  1. 检查链接的topic是否和订阅的一致
  2. 检查username 和 password 是否填写正确

可以链接上本地的但是无法链接线上

让后端检查一下他们的部署是否存在一些问题

意外收获

  1. 如果需要在dev模式链接websocket,需要在webpack中的devServer 设置
client:{webSocketURL: 'auto://0.0.0.0:0/ws'}

,这里的URL可以直接写你要链接的,如果是需要在代码中自动识别,就按照我的写法

  1. mqtt协议必须进行Broker才可以,如果在前端使用mqtt创建链接,想要直接链接到后台,可以链接成功,但是后台的数据是无法传递过来的,因为数据结构会有问题,不符合MQTT协议的规则,但是后台发起的消息石沉大海。