MQ TT的基础与GO语言的实现

215 阅读5分钟

什么是MQ TT ?

是基于发布/订阅模型轻量级的传输协议

应用在哪里 ?

物联网、移动互联网、智能硬件、智慧城市、远程医疗、电力、能源等领域

优点 ?

  1. 轻量级(lightweight)别问为什么用英文,因为最近看英文文档light不是“灯”的意思吗?后来才知道还有少量的,也顺便让我装一下,我比较装您见谅
  2. 节省带宽(以前带宽很贵)
  3. 支持 QoS(设备网络环境复杂)
  4. 不关心PayLoad 不关心数据是什么 也可以是音频、文本、JSON、图片...
  5. 有持续感知会话的能力 知道对方是否在线

QOS(服务质量--全称Quality Of Service)

  • QoS 0:消息最多传递一次。 如果当时客户端不可用,则会丢失该消息。发布者发送一条消息之后,就不再关心它有没有发送到对 方,也不设置任何重发机制。(有可能消息会丢失)

  • QoS 1:消息传递至少 1 次。 包含了简单的重发机制,发布者发送消息之后等待接收者的 ACK,如果没收到 ACK 则重新发送消息。 这种模式能保证消息至少能到达一次,但无法保证消息重复。 (会接收到一次消息但是不保证会不会重复)

  • QoS 2:消息仅传送一次。 设计了重发和重复消息发现机制,保证消息到达对方并且严格只到达一次。(只会接收一次消息)

清除会话(Clean Session)机制

对于那些想重新连接后,收到离线期间错过的消息的客户端,可在连接时设置关闭清除会话,此时服务端将会为客户端存储订阅关系及离线消息,并在客户端重新连接后发送给客户端。

安全双向通信

客户端和服务端不需要直接建立连接进行通讯,也不需要同时在线,而是由消息服务器进行消息的分发

MQ TT支持 TLS/SSl来确定安全通讯,同时在MQTT协议当中提供了客户端的ID、用户名、密码在应用层确立身份授权和验证

在线状态感知

为了应对网络环境不稳定的情况下,MQTT提供了心跳保活(Keep Alive) 机制, 客户端与服务端长时间没有进行消息通讯,Keep Alive保持连接不被断开,若一断开客户端能即刻感知到实行重新连接

同时MQTT还有 LastWill(遗嘱消息),让服务端发现客户端异常下线的情况下,会发送一条信息到特定的MQTT主题(Topic)当中

另外,部分 MQTT 服务器如 EMQX 也提供了上下线事件通知功能,当后端服务订阅了特定主题后,即可 收到所有客户端的上下线事件,这样有助于后端服务统一处理客户端的上下线事件

MQTT和消息队列的区别

他们都是采用发布/订阅模型进行数据通讯的,消息队列主要应用在服务端之间的消息存储和传递,这类通常是数据量大客户端少的情况,MQTT更适合海量的客户端接入管理与信息传输

不同于消息队列的主题、MQTT的主题是不需要主动创建的,MQTT客户端在订阅和发布的时候就会创建,也不需要开发者主动删除,如果没有订阅,没有发布这个主题那就不存在了

如何使用go进行MQTT发送消息和订阅消息

Tips: 你需要使用就将下列所有的代码放在同一个go文件中即可

GO语言下载MQTT的SDK

go get github.com/eclipse/paho.mqtt.golang
go get github.com/gorilla/websocket
go get golang.org/x/net/proxy

初始化mqtt客户端


import (
    mqtt "github.com/eclipse/paho.mqtt.golang"
    "time"
)
// 关于连接的配置
const (
    QPS           = 1
    SERVERADDRESS = "http://127.0.0.1:1883"
    DELAY         = time.Second
    CLIENTID      = "A"
    WRITETOLOG    = true // 如果为true那么published的消息会被打印到控制台
)

func NewMqttClient() mqtt.Client {
    opts := mqtt.NewClientOptions()
    opts.AddBroker(SERVERADDRESS)
    opts.SetClientID(CLIENTID)

    opts.SetOrderMatters(false) // 允许消息无序发送(除非有序传递是必要的
    opts.ConnectTimeout = time.Second
    opts.WriteTimeout = time.Second
    opts.KeepAlive = 10
    opts.PingTimeout = time.Second

    // 自动化连接管理 在网络断开后重新连接
    opts.ConnectRetry = true
    opts.AutoReconnect = true

    // 当连接丢失的时候触发的回调函数
    opts.OnConnectionLost = func(cl mqtt.Client, err error) {
       // 可能是用于断开连接后做的事情
    }
    opts.OnConnect = func(mqtt.Client) {
       // 连接成功后做的事情
    }
    opts.OnReconnecting = func(mqtt.Client, *mqtt.ClientOptions) {
       // 处理重新连接的业务
    }
    client := mqtt.NewClient(opts)
    if token := client.Connect(); token.Wait() && token.Error() != nil {
       panic(token.Error())
    }

    return client
}

定义mqtt的发送消息和订阅消息的异常处理

type MQTT struct {
    Client mqtt.Client
}

// Pub 发送消息
func (m MQTT) Pub(topic string, qos byte, retained bool, message any) {
    // retained为true的话会保留最新消息的副本,在有新的客户端接入时就会发送给他拿到最新的消息
    msg, err := json.Marshal(message)
    if err != nil {
       fmt.Println("解析为json数据错误", err.Error())
       return
    }
    if WRITETOLOG {
       fmt.Printf("发送的消息: %s\n", msg)
    }
    t := m.Client.Publish(topic, qos, retained, msg)
    go func() {
       _ = t.Wait()
       if t.Error() != nil {
          fmt.Printf("发布消息失败: %s\n", err)
       }
    }()
}

// Sub 订阅消息
func (m MQTT) Sub(topic string, qos byte, callback func(client mqtt.Client, msg mqtt.Message)) {
    t := m.Client.Subscribe(topic, qos, callback)
    go func() {
       _ = t.Wait()
       if t.Error() != nil {
          fmt.Println("订阅异常 ", t.Error())
          return
       }
    }()
}

应用

func SubMessage() {
    client := NewMqttClient()
    mq := MQTT{
       client,
    }
    responseChan := make(chan *Message, 1)
    mq.Sub("你的topic", 1, func(client mqtt.Client, msg mqtt.Message) {
       var response Message
       if err := json.Unmarshal(msg.Payload(), &response); err != nil {
          log.Println("解析mqtt响应数据失败: ", err.Error())
          return
       }
       if response.Method == "自己定义你需要读的数据的条件" {
          // 将满足条件的数据写入信道
          responseChan <- &response
       }
    })

    // 等待 MQTT 回复或超时
    select {
    case <-time.NewTimer(2 * time.Second).C:
       // 超时,断开连接并返回空值
       fmt.Println("mqtt连接超时")
       return
    case response := <-responseChan:
       // 接收到 MQTT 回复,返回满足条件的数据
       // TODO:: 处理接收到的数据
       fmt.Println(response)
    }
}

未完成

后续会使用docker来部署消息服务器

  1. 做定时器发送消息到消息服务器中
  2. 订阅发送时给的topic并输出