MQTT基础知识

为什么物联网使用MQTT,而不是HTTP等协议呢?

核心原因在于物联网资源设备受限,而且网络环境不稳定,但是又需要实现高效、可靠的消息传输。

而MQTT协议恰恰可以满足这一点。

1、MQTT全称 Message Queuing Telemetry Transport,即消息队列遥测传输协议,1999年由IBM发布,MQTT的本质是一种应用层通信协议,传输层基于TCP/IP协议,与我们常说的Modbus、HTTP是一样的。

2、MQTT是一种轻量级、低开销的协议,MQTT报文最小只有2字节,远小于其他协议,它的报文是基于二进制的格式,这样会显著降低带宽占用,非常适合物联网中蜂窝网络或低功耗设备场景。

3、MQTT是一种基于发布/订阅模式的协议。

这个模式与公众号非常相似。我写完这篇文章,通过平台发表(即发布)之后,关注(即订阅)本公众号,就能收到本公众号推送的文章。

4、MQTT通过QoS(Quantity of Service)分级来实现可靠的消息传递,提供了至多一次、至少一次、恰好一次三级服务质量,对于重要性不同的消息采用不同的QoS来平衡实时性与可靠性。


MQTT通信模型

我们前面提到,MQTT协议是通过发布/订阅的方式来实现的,消息的发布方与订阅方通过这种方式来进行解耦,因此,需要有一个中间方来进行消息的转发及存储。

在MQTT协议中,这个中间方我们称之为Broker(可以理解为服务器Server),而连接到Broker的订阅方和发布方称之为Client。

终于有人把MQTT通信说明白了_Text

以上是一次典型的MQTT协议消息通信流程:

1、发布方与订阅方都与Broker建立了TCP连接。

2、订阅方告知Broker订阅的消息主体(Topic)。

3、发布方将带有主题(Topic)的消息发布到Broker。

4、Broker收到消息后,检查有哪些订阅方订阅了该Topic,然后将消息发给这些订阅方。

5、订阅方从Broker中获取该消息。


MQTT环境搭建

我们学习或使用Modbus的时候,可以使用相关的调试软件进行调试。

Modbus调试软件包括ModbusPoll(模拟主站或客户端)与ModbusSlave(模拟从站或服务器)。

MQTT也有对应的软件,一般Broker的模拟软件有Mosquitto(免费)、EMQ X(商业)等,除了使用软件,我们还可以使用阿里云、腾讯云之类的云服务器,这些云服务器商一般会提供物联网云平台。

基于Mosquitto搭建的MQTT Broker运行如下:

终于有人把MQTT通信说明白了_Text_02

对于MQTT Client,我们一般可以使用mqttfx软件来实现。

我们可以使用mqttfx直接连接上面的Broker,测试一下MQTT通信。

连接成功后,我们向home/garden/fountain这个主题发布一个消息:Test mqtt by xbd,此时的mqttfx是发布者的角色。

终于有人把MQTT通信说明白了_Click_03

我们切换到订阅者,订阅一下这个主题home/garden/fountain。

这样我们就可以收到这个消息内容了。

终于有人把MQTT通信说明白了_Text_04

这样我们就基于相关软件体验了一次完整MQTT通信的过程。

MQTT通信协议

MQTT与Modbus一样,是一个应用层协议,既然是应用层协议,就有相关的协议文档及报文格式。

与Modbus不同的是,MQTT协议使用的是二进制数据包,Modbus最小单位是字节,而MQTT协议会精确到每个二进制位的含义,这个其实也是与MQTT的应用场景有关,MQTT定位就是轻量级、低开销,因此需要尽量保证报文较短。

MQTT协议的数据包并不复杂,一个MQTT协议数据包由三个部分组成,分别是固定头、可变头及消息体。

固定头

Fix Header

可变头

Variable Header

消息体

Payload

2-5 Byte

N Byte

N Byte

固定头:这部分是必须的,包含数据包类型、标识位及剩余包长度。

可变头:这部分非必须,具体内容与数据包类型对应。

消息体:这部分非必须,存储消息的具体数据。

具体MQTT协议的细节内容,大家可以参考MQTT官方协议文档。

在实际开发中,我们一般不需要了解协议,可以直接使用开源的MQTT库。

MQTT通信开发

我们这里使用MQTTnet这个开源库。

我们开发一个MQTTClient,界面如下:

终于有人把MQTT通信说明白了_Click_05

1、UI界面设计完成后,首先通过Nuget搜索MQTTnet并安装。

终于有人把MQTT通信说明白了_客户端_06

2、建立MQTT连接及断开,代码如下:

private void btn_Connect_Click(object sender, EventArgs e)
{
    var option = new MqttClientOptions() { ClientId = Guid.NewGuid().ToString("D")
    //设置连接的服务器及端口号
    option.ChannelOptions = new MqttClientTcpOptions()
    {
        Server = this.cmb_ServerIP.Text,
        Port = Convert.ToInt32(this.txt_Port.Text)
    };
    //是否需要账户密码
    if (this.chk_IsUsePwd.Checked)
    {
        option.Credentials = new MqttClientCredentials()
        {
            Username = this.txt_UserName.Text,
            Password = this.txt_Pwd.Text
        };
    }
    option.CleanSession = true;
    option.KeepAlivePeriod = TimeSpan.FromSeconds(100);
    option.KeepAliveSendInterval = TimeSpan.FromSeconds(10000);
    if (mqttClient != null)
    {
        mqttClient.DisconnectAsync();
        mqttClient = null;
    }
    try
    {
        mqttClient = new MqttFactory().CreateMqttClient();
        mqttClient.Connected += MqttClient_Connected;
        mqttClient.Disconnected += MqttClient_Disconnected;
        mqttClient.ApplicationMessageReceived += MqttClient_ApplicationMessageReceived;
        mqttClient.ConnectAsync(option);
    }
    catch (Exception ex)
    {
        AddLog(1, "MQTT客户端连接失败:" + ex.Message);
        return;
    }
}
 private void btn_DisConn_Click(object sender, EventArgs e)
 {
     mqttClient.DisconnectAsync();
 }

3、对于连接、断开连接及收到消息会有对应的事件处理,代码如下:

private void MqttClient_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e)
{
    this.AddLog(0, "MQTT客户端收到信息:Topic:" + e.ApplicationMessage.Topic + " Payload:" + Encoding.UTF8.GetString(e.ApplicationMessage.Payload));
}
private void MqttClient_Disconnected(object sender, MqttClientDisconnectedEventArgs e)
{
    AddLog(0, "MQTT客户端断开连接:ClientWasConnected:" + e.ClientWasConnected);
}
private void MqttClient_Connected(object sender, MqttClientConnectedEventArgs e)
{
    AddLog(0, "MQTT客户端连接成功:IsSesstionPresent:" + e.IsSessionPresent);
}

4、订阅和取消订阅,可以直接调用相关方法,代码如下:

private void btn_Sub_Click(object sender, EventArgs e)
{
    try
    {
        mqttClient.SubscribeAsync(new List<TopicFilter>()
    {
        new TopicFilter(this.txt_Topic.Text,(MqttQualityOfServiceLevel)Enum.Parse(typ
(MqttQualityOfServiceLevel), this.cmb_QOS.Text,true))
    });
    }
    catch (Exception ex)
    {
        AddLog(1, "MQTT客户端订阅失败:" + ex.Message);
        return;
    }
    AddLog(0, "MQTT客户端订阅成功:Topic:" + this.txt_Topic.Text);
}
private void btn_UnSub_Click(object sender, EventArgs e)
{
    try
    {
        mqttClient.UnsubscribeAsync(this.txt_Topic.Text);
    }
    catch (Exception ex)
    {
        AddLog(1, "MQTT客户端取消订阅失败:" + ex.Message);
        return;
    }
    AddLog(0, "MQTT客户端取消订阅成功:Topic:" + this.txt_Topic.Text);
}

5、发布消息,可以直接调用相关方法,代码如下:

private void btn_Publish_Click(object sender, EventArgs e)
{
    if (mqttClient == null)
    {
        AddLog(1, "客户端未连接,无法发送消息");
        return;
    }
    var msg = new MqttApplicationMessage()
    {
        Topic = this.txt_Topic.Text,
        Payload = Encoding.UTF8.GetBytes(this.txt_Message.Text),
        QualityOfServiceLevel = (MqttQualityOfServiceLevel)Enum.Parse(typeof(MqttQualityOfServiceLevel), this.cmb_QOSPublish.Text, true),
        Retain = false
    };
    try
    {
        mqttClient.PublishAsync(msg);
    }
    catch (Exception ex)
    {
        AddLog(1, "MQTT客户端发布失败:" + ex.Message);
        return;
    }
    AddLog(0, "MQTT客户端发布成功:Topic:" + this.txt_Topic.Text + " Payload" + this.txt_Message.Text);
}

6、完成以上功能后,我们就可以进行测试,测试结果如下:

我们首先将软件复制一份。

第一个连接Broker,作为发布者角色,第二个连接Borker,作为订阅者角色。

订阅者订阅主题xbd/swj,发布者向这个主题发送消息内容,当我们发布这个内容Test MQTT By XBD,我们可以看到订阅者收到了这个消息,这样就实现了MQTT通信。

终于有人把MQTT通信说明白了_Click_07

终于有人把MQTT通信说明白了_客户端_08