50-STM32+ESP8266+AIR202基本控制篇-重点详解-MQTT协议

2020-07-03 15:56:28 浏览数 (1)

先来体验一下MQTT通信

这是我制作的一个上位机MQTT调试助手,提供了源码.

注:需要打开两个,默认连接提供的服务器测试. 第一个配置如下: 发布的主题:aaaaa 订阅的主题:Topic 点击连接,然后点击订阅

第二个配置如下: 发布的主题:Topic 订阅的主题:aaaaa 点击连接,然后点击订阅

第一个软件发消息:发送的消息123456,然后点击发送

用户会看到第二个软件收到消息 提示:这个软件是自己开发的,里面的显示都是自己规定的.

其实过程是这样的:

两个客户端都连接了一个MQTT服务器(这个只是个软件,后面章节会告诉大家怎么安装)

第一个客户端发布的主题那一栏填写的是 aaaaa  然后发送的消息是 123456

点击发送的时候实际上该消息就发给了MQTT服务器

整个消息格式呢大概是这样的 XXXXaaaaaXXXX123456

XXXX呢代表其它信息,方便服务器区分出来整个消息中的

发布的主题(aaaaa)和发布的消息(123456)

其实 aaaaa 就充当了这个消息的标识

第二个客户端的订阅那一项填写的是 aaaaa

其实就是在告诉服务器,我需要数据标识是 aaaaa的消息

既然你告诉了服务器了,那么服务器只要接收到数据标识是 aaaaa

的消息,那么就会主动把消息发给你

同理,让下面的客户端把消息发给上面的客户端

简要说明:

连接上MQTT服务器以后,只要是两个设备之间订阅和发布的主题对上了

那么这两个设备就可以通信了

对于初学者可能疑惑,你软件点点点到底内部是怎么做到的

如果你想知道更多就接着看,我先说明一下过程.

其实MQTT就是一个TCP服务器,它是在TCP通信的时候封装了一套协议.

咱们就叫它MQTT协议,注意本质上就是TCP传输数据,这个数据有格式而已!

首先是使用TCP连接,然后发送MQTT连接协议,然后发送MQTT订阅主题的协议.

这样的话,服务器就知道你需要哪种标识的数据了.

当服务器收到这种标识的数据的时候,服务器就会主动转发给你.

其实MQTT服务器主要工作就是做数据转发,但是你需要告诉它你需要什么样的数据.

思考

1,其实理解一个东西最好的方式就是:你要设想如果让你自己做一个这样的服务器,你会怎么做.

2,现在需求是做一个负责数据转发的软件

首先,平时的时候咱做的TCP服务器都是,一个或者多个客户端连接咱做的TCP服务器,然后TCP服务器处理客户端的数据.

现在呢!需求变了!

假设我有5个网络设备,3个手机.我现在想让网络设备把数据远程传给手机.而且我还需要记录网络设备上传的数据.

假设通信是这样的(而且后期还会不停的增加设备和手机)

3,咋办???

  1. 需要记录所有设备的数据

  2. 设备和手机之间存在多对一和一对多

  所以,必须需要个公共的服务器进行数据的中转.

  假设你就把这个服务器做成TCP服务器,有人问,你咋不做成UDP呢?

       UDP他妹的是无连接状态,发送数据不好判断是不是发送成功,我还是少找些麻烦!

  还有就是要实现远程,有个公网IP就可以,可以自己买个服务器,上网络公司拉一根专网

  或者用自己电脑,用花生壳映射

  还是用云服务器吧!就是运行在别人的服务器上的一台电脑(就是一台电脑),IP地址直接是公网.方便.

4,怎么设计这个TCP服务器???

  1.为了应对这种通信,首先设备发送的数据决不能是单单的数据,必须加点东西

  2.如果把发送的数据带上标识呢? 假设设备1发送的数据是:   aaaaa数据   (aaaaa是数据标识,后面是真实数据)

  3.然后呢!假设手机1就接收数据标识是aaaaa的数据,怎么让服务器转发给它呢???

  4.如果手机1在连接上TCP服务器的时候 告诉TCP服务器我接收数据标识是 aaaaa的数据

  5.通过上面的方式是不是有点眉头了????

  咱呢姑且把 "告诉TCP服务器我接收数据标识是 aaaaa的数据"  这个事情呢,起个名字     订阅的主题是 aaaaa

  把 "假设设备1发送的数据是:  aaaaa数据 "    消息前面的 aaaaa 叫做 发布的主题是aaaaa

5,总结上面的就是

  手机1先连接TCP服务器,然后呢,规定个协议,告诉TCP服务器我订阅的主题是aaaaa

  这样呢服务器就记住了,当出现消息前面的主题是aaaaa的消息的时候,他就把这个消息发给手机1

  当然咱假设,设备1连接上TCP服务器,然后,告诉TCP服务器我订阅的主题是wwww

  这样呢服务器就记住了,当出现消息前面的主题是wwww的消息的时候,他就把这个消息发给设备1

  然后设备1连接上TCP服务器以后呢,这样发送信息(假设发送的消息是123456):   aaaaa123456

  服务器一接收到客户端的消息,就取出来这个消息的标识是什么,取出来的是 aaaaa

  然后呢,看下记录的谁需要消息标识是aaaaa的消息,然后找到了手机1

  最后把这个消息发送给手机1这个客户端,然后手机1就接收到了1123456这个消息

  同理:手机1发送  wwww998877  然后这个消息就会发给设备1 ,设备1就会收到 998877

6,总结

  这个服务器道理上是这样,服务器记录各个设备的信息,各个设备订阅的主题,然后呢,判断这个消息然后进行转发

  但是...咱做个简单的完全可以做出来,但是要想做的完善,而且要支持庞大消息数量的设备(来个百万级).....不是一朝一夕就可以的.

  其实很长时间以前,人们就有这种需求了.多对一和一对多通信

  所以呢,一些组织和单位就开始解决这种问题,开始做这种软件,所以MQTT就诞生了.

  之所以叫MQTT是因为是外国人做的这种TCP服务器,外国人呢,为实现这种功能的TCP服务器取了个名字叫

  Message Queuing Telemetry Transport

  然后取每个首字母  就叫 MQTT了

  其实有很多家做MQTT软件,但是呢,我比较喜欢用emqtt

来聊一下具体的MQTT协议了

一,首先咱知道就是个TCP服务器,所以呢,需要先用TCP连接上他们的服务器.

二,咱用Android ,C#,QT,网页等等连接MQTT服务器的时候有现成的封装好的库可以用

 其实说白了就是调用函数而已.....

三,但是对于单片机而言要想实现MQTT通信,那么就需要借助网络模块

    大部分的网络模块都可以实现TCP通信,咱呢,就需要在TCP的基础上按照MQTT协议封装下咱的数据

    注:其实官方给了现成的MQTT的封装数据和解析数据的程序)

四,咱利用网络模块的TCP连接上以后

 然后需要发送第一条消息(注:并不是上来就可以订阅主题的)

 MQTT软件规定呢,你发送的第一条信息是连接信息(相当于咱要先登录)

 他规定了几个参数!

 ClientID: 各个客户端必须设定一个ID,各个客户端必须都不一样               假设是 123456

 用户名: 咱安装MQTT软件的时候可以设置MQTT软件的登录的用户名     假设是yang

 密码: 咱安装MQTT软件的时候可以设置MQTT软件的登录的密码             假设是 11223344

 下面是我当初研究MQTT的协议写的,然后把上面三个参数填进去

 注意这节我粘贴的代码只是为了大家了解协议,该文章封装的包不完整,请不要使用

 咱板子是用的STM32,里面使用的库是我专门为单片机封装的.

https://docs.emqx.io/sdk_tools?category=MQTT_Clients   (官方提供的各个开发的库)

单片机用下面这个,我当前MQTT程序的库就是用的这个,不过后来舍弃了,重新自己封装的

不是因为不好用,而是因为占用内存太大!

五,MQTT连接协议

代码语言:javascript复制
/**
* @brief  连接服务器的打包函数
* @param  
* @retval 
* @example 
**/
int ConnectMqtt(char *ClientID,char *Username,char *Password)
{
    int ClientIDLen = strlen(ClientID);
    int UsernameLen    = strlen(Username);
    int PasswordLen = strlen(Password);
    int DataLen = 0;
    int Index = 2;
    int i = 0;
    DataLen = 12   2 2 ClientIDLen UsernameLen PasswordLen;
    MqttSendData[0] = 0x10;                //MQTT Message Type CONNECT
    MqttSendData[1] = DataLen;    //剩余长度(不包括固定头部)
    MqttSendData[Index  ] = 0;        // Protocol Name Length MSB    
    MqttSendData[Index  ] = 4;        // Protocol Name Length LSB    
    MqttSendData[Index  ] = 'M';        // ASCII Code for M    
    MqttSendData[Index  ] = 'Q';        // ASCII Code for Q    
    MqttSendData[Index  ] = 'T';        // ASCII Code for T    
    MqttSendData[Index  ] = 'T';        // ASCII Code for T    
    MqttSendData[Index  ] = 4;        // MQTT Protocol version = 4    
    MqttSendData[Index  ] = 0xc2;        // conn flags 
    MqttSendData[Index  ] = 0;        // Keep-alive Time Length MSB    
    MqttSendData[Index  ] = 60;        // Keep-alive Time Length LSB  60S心跳包  
    MqttSendData[Index  ] = (0xff00&ClientIDLen)>>8;// Client ID length MSB    
    MqttSendData[Index  ] = 0xff&ClientIDLen;    // Client ID length LSB  

    for(i = 0; i < ClientIDLen; i  )
    {
        MqttSendData[Index   i] = ClientID[i];          
    }
    Index = Index   ClientIDLen;
    
    if(UsernameLen > 0)
    {   
        MqttSendData[Index  ] = (0xff00&UsernameLen)>>8;//username length MSB    
        MqttSendData[Index  ] = 0xff&UsernameLen;    //username length LSB    
        for(i = 0; i < UsernameLen ; i  )
        {
            MqttSendData[Index   i] = Username[i];    
        }
        Index = Index   UsernameLen;
    }
    
    if(PasswordLen > 0)
    {    
        MqttSendData[Index  ] = (0xff00&PasswordLen)>>8;//password length MSB    
        MqttSendData[Index  ] = 0xff&PasswordLen;    //password length LSB    
        for(i = 0; i < PasswordLen ; i  )
        {
            MqttSendData[Index   i] = Password[i];    
        }
        Index = Index   PasswordLen; 
    }    
    return Index;
}

假设我ClientID填写的是:123456

UserName填写的是:yang

Password填写的是:11223344

执行以后得到以下数据

10 22 00 04 4D 51 54 54 04 C2 00 78 00 06 31 32 33 34 35 36 00 04 79 61 6E 67 00 08 31 31 32 32 33 33 34 34

然后把这个数据发给TCP 服务器,如果没有错误,服务器就会回 20 02 00 00

咱可以用TCP调试助手试一试

先连接我的哈,后面章节有说明怎么安装MQTT服务器

<>

<>

<>

IP地址:47.92.31.46  注意:如果IP不可以连接可以填域名 mnifdv.cn 端口号:1883 <>

先说一件事情  所有的MQTT数据哈  第一个字节是说明整个数据是干什么的数据   第二个字节是说它后面的数据的总个数

10 : 固定,MQTT规定的连接用0x10

22: 是说0x22后面有0x22个数据  34个

00 04: 后面记录MQTT版本号的字节个数  

4D 51 54 54: M Q T T  版本号字符         这个是4版本,不同版本不一样 3版本的是MQIsdp 额,了解就可以

04: 版本号是 0x04

C2:这个呢想了解具体呢,需要看协议  http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028

<>

C2 :1100 0010

bit7 bit6:是否有用户名和密码

bit5 :遗嘱是否需要服务器保留

bit4 bit3:遗嘱的消息等级

bit2:是否设置了遗嘱

bit1:是否清除以前的连接信息

bit0:保留,默认0

上面就是说有用户名和密码,每次连接的时候清除连接信息,没有设置遗嘱(后面会说)

00 78: 心跳包是120S一次(这个自己连接的时候自己设置),

MQTT规定客户端必须发心跳包,客户端发送的心跳包数据是 0xC0 0x00,这是MQTT规定的

如果心跳包间隔了你设定心跳包的1.5倍时间,你没有发给服务器,服务器就认为你掉线了,这是关于遗嘱的问题,,后面会说

你发给服务器 0xC0 0x00  服务器会回你 0xD0  0x00  这个知道就行了

00 06:客户端的ClientId有6位

后面的  31 32 33 34 35 36    就是ClientId ,这是MQTT服务器规定的,每个客户端必须有各自的ClientId

00 04: MQTT的用户名  

79 61 6E 67  我安装MQTT的时候设置的MQTT的用户名是yang

00 08: MQTT的密码

31 31 32 32 33 33 34 34  我安装MQTT的时候设置的MQTT密码

好了,总结下就是连接上TCP服务器 然后发送

10 22 00 04 4D 51 54 54 04 C2 00 03 00 06 31 32 33 34 35 36 00 04 79 61 6E 67 00 08 31 31 32 32 33 33 34 34

服务器呢就会回你  20 02 00 00

20: 固定

02: 后面有两个数据

00 00

注意后面的第一个数据 00 ,如果你设置了 Clean Session为1 :便会回复 01

最后一个数据呢,有几个返回值,0就说明成功,其它就是有各种问题

比如说回的是 20 02 00 04  就说明用户名或者密码有问题.

<>

六,订阅主题

假设告诉服务器我订阅的是2222

假设订阅的时候订阅的主题的消息标识是1,消息等级是0

那么打包以后就是 82 09 00 01 00 04 32 32 32 32 00  然后把这个数据发给TCP服务器

让测试用TCP调试助手订阅,然后用咱的MQTT调试助手发信息给咱的TCP调试助手

注意:现在咱的TCP可能已经断开了,因为咱的TCP调试助手没有在规定时间内发送心跳包

首先准备好

<>

首先连接

10 22 00 04 4D 51 54 54 04 C2 00 03 00 06 31 32 33 34 35 36 00 04 79 61 6E 67 00 08 31 31 32 32 33 33 34 34

<>

然后订阅

82 09 00 01 00 04 32 32 32 32 00

然后用MQTT调试助手发消息

<>

<>

订阅主题

代码语言:javascript复制
/**
* @brief  MQTT订阅/取消订阅数据打包函数
* @param  SendData 
* @param  topic                主题 
* @param  qos         消息等级 
* @param  whether     订阅/取消订阅请求包
* @retval 
* @example 
**/
int MqttSubscribeTopic(char *topic,u8 qos,u8 whether)
{    
    int topiclen = strlen(topic);
    int i=0,index = 0;
  
    if(whether)
        MqttSendData[index  ] = 0x82;                        //0x82 //消息类型和标志 SUBSCRIBE 订阅
    else
        MqttSendData[index  ] = 0xA2;                        //0xA2 取消订阅
    MqttSendData[index  ] = topiclen   5;                //剩余长度(不包括固定头部)
    MqttSendData[index  ] = 0;                          //消息标识符,高位
    MqttSendData[index  ] = 0x01;                    //消息标识符,低位
    MqttSendData[index  ] = (0xff00&topiclen)>>8;    //主题长度(高位在前,低位在后)
    MqttSendData[index  ] = 0xff&topiclen;              //主题长度 
    
    for (i = 0;i < topiclen; i  )
    {
        MqttSendData[index   i] = topic[i];
    }
    index = index   topiclen;
    
    if(whether)
    {
        MqttSendData[index] = qos;//QoS级别
        index  ;
    }
    return index;
}

假设上面的MqttSubscribeTopic("2222",0,1)

0x82: 告诉MQTT服务器,我要订阅主题

0x09: 后面的数据个数

0x00  0x01  注意哈,订阅主题的时候可以设置了标识  标识呢  1-65535

之所以有这个家伙:咱订阅的时候怎么判断订阅成功了呢???

订阅成功以后呢!服务器会返回咱订阅成功的回复,回复里面就包含着咱写的这个标识

咱呢可以对比下这个标识,然后呢就知道到底是不是订阅成功了.

0x00  0x04  后面订阅主题的长度

32 32 32 32 订阅的主题是 2222

最后一个 00 是说消息等级,一般呢,订阅设置为0 就可以

那就说一下这个消息等级有什么用吧!

咱发送数据的时候也会携带一个消息等级

假设是0  那么这条消息是不是真的发给MQTT服务器(Broker)了,就不知道了,

假设是1 那么一个客户端发送消息以后呢,服务器一看消息等级是1,那么就会回给那个发送消息的客户端一个应答消息

客户端可以根据有没有回复应答确认发没发送成功

假设是2 这个呢服务器和客户端之间会有双向的应答!后面会详细说.

如果按照上面发呢,服务器会回

90 03 00 01 00

90:固定

03:后面的数据长度

00 01:这条主题的标识

00:消息等级

如果订阅多个主题假设订阅两个主题 消息等级第一个是0 第二个是1

90 04 00 01 00 01

90:固定

03:后面的数据长度

00 01:这条主题的标识

00:消息等级

01:消息等级

假设订阅失败

后面的消息等级就会变为 0x80 (订阅一个主题)

90 03 00 01 00

90:固定

03:后面的数据长度

00 01:这条主题的标识

80:消息等级变为0x80

订阅两个主题,第一个订阅失败

后面的消息等级就会变为 0x80

90 03 00 01 00

90:固定

04:后面的数据长度

00 01:这条主题的标识

80:消息等级

00:消息等级

七,发布消息

长话短说

发布的时候呢,信息里面都有以下内容

发布的主题,消息,消息等级,是不是需要服务器保留消息,消息的标识

代码语言:javascript复制
/**
* @brief  MQTT发布数据打包函数
* @param  mqtt_message 
* @param  topic                主题 
* @param  qos         消息等级 
* @retval 
* @example 
**/
int MqttPublishData(char * topic, char * message, u8 qos)
{  
    int topic_length = strlen(topic);    
    int message_length = strlen(message);  
    int i,index=0;    
    static u16 id=0;
    
    MqttSendData[index  ] = 0x30;    // MQTT Message Type PUBLISH  30:消息等级是0  32消息等级是1  34消息等级是2

  
    if(qos)
        MqttSendData[index  ] = 2   topic_length   2   message_length;//数据长度
    else
        MqttSendData[index  ] = 2   topic_length   message_length;   // Remaining length  

  
    MqttSendData[index  ] = (0xff00&topic_length)>>8;//主题长度
    MqttSendData[index  ] = 0xff&topic_length;
         
    for(i = 0; i < topic_length; i  )
    {
        MqttSendData[index   i] = topic[i];//拷贝主题
    }
    index  = topic_length;
        
    if(qos)
    {
        MqttSendData[index  ] = (0xff00&id)>>8;
        MqttSendData[index  ] = 0xff&id;
        id  ;
    }
  
    for(i = 0; i < message_length; i  )
    {
        MqttSendData[index   i] = message[i];//拷贝数据
    }
    index  = message_length;
        
    return index;
}

发布的主题: 谁订阅了这个主题,服务器就会把相应的消息传给谁

消息等级:上面说了

是不是需要服务器保留消息:一会和遗嘱一块说

消息的标识:每条消息加个标识,用来区分消息

八,遗嘱

还记得上面

我直接说遗嘱是啥意思哈!

假设我手机和一个设备订阅主题和发布主题对应,我就能和这个设备通信了

但是,我怎么知道这个设备掉线了呢?

当然完全可以自己发信息给那个设备,如果不回复,就说明掉线了

但是呢!MQTT服务器提供了一种方式

假设我设置好一个设备的遗嘱消息是  offline    遗嘱发布的主题是 aaaaa

另一个设备订阅的主题是 aaaaa

如果设备掉线,服务器就会给订阅了aaaaa的设备发送  offline

还记得上面说的不   服务器如果在你设置的心跳包时间的1.5倍收不到心跳包就认为你掉线了.

九,心跳包

  MQTT规定的,发送完连接协议之后

  发送的心跳包数据是C0 00

  发送时间:连接协议里面的心跳包时间(你可以提前发)

  然后服务器回复 D0 00

扩展

有人会问,如果我想监控所有设备的数据应该怎么做

  就是说,我有个所有设备都可以管理的后台

  假设我是用C#做了一个MQTT的上位机,监控所有的数据

  笨法:

    你订阅的时候把所有设备发布的主题全部订阅一遍

    假设现在其中一个设备,想获取其它连个设备的数据

    其它两个设备发布的主题如下:

      另一个设备

    订阅 aaaaa 然后再订阅 wwww

    然后就可以了

  另一个客户端

MQTT自带的绝招:

    先说一下哈

    假设一个客户端发布的主题是 tttt/aaaaa

    还有一个客户端发布的主题是 tttt/wwww

    如果想让有一个客户端接收他俩的数据

    你只需要订阅  tttt/#

补充(关于MQTT消息等级,DUP)

1.1假设客户端1 发布的主题是 1111 ;消息等级是:0  ;发送的消息是999  最终发送的信息如下:

  30 0b 00 04 31 31 31 31 39 39 39

  消息等级是0是说明该消息发送出去就完事了,服务器不会回复任何应答信息.

  至于该消息发没发给服务器,不知道!

1.2假设客户端2 订阅的主题是:1111  消息等级是 0

  假设客户端1 确实把消息发给了服务器

  客户端2 收到消息以后,不需要做任何操作

2.1 假设客户端1 发布的主题是 1111 ;消息等级是:1  ;发送的消息是999  最终发送的信息如下:

  32 0b 00 04 31 31 31 31 XX XX 39 39 39

  XX XX是在发送的时候需要加上的消息标识符:

  消息标识符XX XX随意即可:范围1-65535

  假设消息标识符是 00 01

  发送完以上消息以后,服务器会回复: (PUBACK) 告诉客户端我收到了

  40 02 00 01

  (00 01就是咱上面发送的消息标识符)

  这样就证明消息确实送达给了服务器

  如果客户端1 发布完消息以后没有接收到服务器的应答

  则可以重新发布消息

  32 0b 00 04 31 31 31 31 XX XX 39 39 39

  XX XX可以和上次的一样,也可以不一样

2.2 假设客户端2订阅了主题是 1111 ;消息等级是:1

  服务器接收到客户端1发送的消息之后,转发给客户端2

  32 0b 00 04 31 31 31 31 XX XX 39 39 39

  注意现在的XX XX(消息标识符)是服务器自己随机生成的了

  假设标识符是 00 02

  客户端2在接收到消息之后需要返回应答(PUBACK) 告诉服务器我收到了

  40 02 00 02

  如果客户端2不回复:40 02 00 02  (后面咱就叫 PUBACK)

  服务器便会一直发送消息给客户端2

  3A 0b 00 04 31 31 31 31 00 02 39 39 39

  注意开头变为了 3A (服务器自动会把重传标志置一)

  高4位是 3 固定

  后面四位:

  第一位:DUP  标记这条消息是不是重传的

  第2,3位:消息等级  01  :消息等级1   10:消息等级2

  最后一位:RETAIN 是否需要服务器保留这条消息

  本来是 32    0011 0010

  变为了 3A    0011 1010

  其实服务器加上DUP是为了让客户端知道,我这条消息是重传的,

  因为服务器第一次发的时候客户端没有返回PUBACK

  但是服务器知道我确实是传给了客户端

  客户端这边假设真的是没有及时的回复PUBACK

  那么有两种方式处理

  2.2.1.再次接收到消息以后,无论消息有没有DUP标志

  直接处理消息

  如果判断这条消息是需要返回 PUBACK的

  那么直接根据消息里面的消息标识符返回 PUBACK 即可

  2.2.2.判断下如果有DUP标志,那么再提取下消息标识

  看一下我先前是不是处理了有相同消息标识符的消息

  如果有就说明我已经处理了,只是没有返回PUBACK

  那么我不去处理这条消息

  直接根据消息里面的消息标识符返回PUBACK就可以

  2.2.3 其实....

  但是整体来说,对于消息等级是1的消息统统处理即可

  然后根据消息里面的消息标识符返回PUBACK即可

  先说一下为什么

  其实在客户端1发布消息等级是1的消息的时候,

  如果客户端1由于某些原因没有接收到服务器的PUBACK

  那么客户端1还会再发布先前的消息

  其实现在就有两条或者多条相同的消息在服务器里面

  这些相同的消息(标识符不一样的消息)就会发给客户端2

  如果客户端2一直不应答(PUBACK),那么服务器便会把所有的没有收到应答的消息

  的DUP标记置一以后不停的发给客户端2...

  直至客户端2应答了所有的消息,或者客户端2断线了

  服务器才停止发送

  对于单片机而言,这些处理只能自己去实现

  为了方便和节省内存,对于消息等级是1的消息

  可以直接根据消息里面的消息标识符返回PUBACK

    所以对于消息等级是1的消息,其实客户端至少会接收到1次消息

3.1 假设客户端1 发布的主题是 1111 ;消息等级是:2  ;发送的消息是999  最终发送的信息如下:

  34 0b 00 04 31 31 31 31 XX XX 39 39 39

  XX XX是在发送的时候需要加上的消息标识符:

  消息标识符XX XX随意即可:范围1-65535

  假设消息标识符是 00 01

  注意:服务器接收到此消息以后并不会立即发送给订阅了主题是1111,消息等级是2的客户端

  服务器接收到以后会返回: PUBREC) "告诉客户端我收到了"

  50 02 00 01

  客户端1需要返回: PUBREL) "好的"

  62 02 00 01

  注意:返回这个以后,消息才会下发给订阅了主题是1111,消息等级是2的客户端

  服务器接着会返回: PUBCOMP)

  70 02 00 01

  很多人介绍QS2都说保证消息只传输一次,其实实际上是这样保证的

  客户端1在发送完消息以后

  34 0b 00 04 31 31 31 31 XX XX 39 39 39

  服务器会返回(PUBREC)

  但是只要客户端1不回复 (PUBREL)

  无论客户端1现在发送多少条消息等级是2的消息

  服务器都不会理会,服务器只会记录你发送的最后一条消息

  客户端1只有回复了(PUBREL)

  服务器才会把最后一条消息转发出去

  最后返回 (PUBCOMP)

3.2 假设客户端2订阅了主题是 1111 ;消息等级是:2

  服务器接收到客户端1发送的消息,  然后确认接收到客户端1的(PUBREL)之后,

  转发给客户端2 :

  34 0b 00 04 31 31 31 31 XX XX 39 39 39

  注意现在的XX XX(消息标识符)是服务器自己随机生成的了

  假设标识符是 00 02

  客户端2接收到以后需要返回: PUBREC) "告诉服务器我收到了" 

  50 02 00 02

  注意:如果客户端2不回复: PUBREC),那么服务器会不停的发送

  34 0b 00 04 31 31 31 31 XX XX 39 39 39

  直至客户端2回复: PUBREC)

  服务器接收到以后会返回: PUBREL) "好的"

  62 02 00 02

  客户端2最后需要返回: PUBCOMP)

  70 02 00 02

  注意:即使客户端2不返回(PUBCOMP),服务器隔一段时间也会默认客户端2回复了(PUBCOMP)

十,补充(关于retain)

发布消息的时候,第一个字节的最后一位代表 Retain

意思是,是否需要服务器保留这个消息

如果设置了服务器保留了这个消息,那么只要客户端订阅了这个消息的主题

服务器就会立马发送给客户端保留的这个消息

详细说明:

假设我发布的主题是:1111 消息是:999 ,消息等级随意(假设是0)  然后设置了 Retain位置为1

31 09 00 04 31 31 31 31 39 39 39

客户端1把这条消息发给了服务器

然后过了一会,客户端2上线了

订阅了主题 1111

因为上面的消息设置了让服务器保留

所以只要客户端2 一订阅1111,便会收到 999这个消息

这就是Retain的作用

当然,有更巧妙的应用

我一般用这个来发送在线或者掉线,或者开关量数据

1.我设置客户端1的遗嘱发布的主题是 1111  遗嘱消息是 offline   Retain为1

2.我设置客户端1连接上服务器以后先发布一条消息

发布的主题也是 1111  消息是: online     Retain为1

注意:服务器最终只会保留最后一条需要保留的消息

只要是另一个客户端订阅 1111

如果客户端1是掉线的,那么便会立即收到 offline

如果客户端1是在线的,呢么便会立即收到 online

有些时候咱控制开关,咱打开上位机以后想立即知道开关的状态

最好的方式就是把发送开关亮数据的主题 Retain设置为1

那么只要是上位机一订阅设备发布的开关量主题,

便会立即得到开关量数据!

这样便提高了用户体验.

结语

以上说的,对于高级语言已经封装好的包而言,发布或者接收消息等级1/2,

需要做的后续操作,高级语言里面已经做了处理

但是对于单片机而言,需要自己处理

补充:

昨天有个人测试MQTT,发现只要设备连接上MQTT,不需要订阅,就能接收到服务器的消息

然后他就有点懵!

我说一下,其实这个功能也是属于MQTT的范畴!

大家看MQTT协议,只知道订阅了某个主题就可以收到某个主题的信息

注意:MQTT协议中并没有说只有订阅才可以收到!

我说一下:其实MQTT就是个TCP服务器,MQTT客户端就是个TCP客户端

其实大家要从大的方向上去考虑,整个的通信就分为两层.

第一层是TCP通信

第二层是解析TCP数据

就是这样而已!!!!

我问一下,我TCP服务器可以给TCP客户端主动发信息吧???

其实就是上面说的,不需要订阅就可以接收到信息!

有些人会想,怎么会?

不订阅为啥可以接收?

其实你只是跟着别人学了个表面东西,然后给自己设定了一个错误的思路:只有订阅才能接收信息!

服务器可以主动推送信息给各个客户端!只要是信息格式是正确的就可以

因为TCP接收到信息以后就是解析一下是不是MQTT协议格式的数据而已!

有些人会想,为啥客户端解析协议里面不判断下是不是自己订阅的呢??

这个是你自己根据自己的需求去做的事情!

你要想让你的协议兼容到各个使用场景,你说你是做成开放形式还是封闭的?

你可能咋一听感觉为啥还能这样!

其实本来就是有的东西,只是你以前不了解而已!

0 人点赞