LWIP之MQTT应用协议~

MQTT简介

MQTT 是一种基于客户端服务端架构的发布/订阅模式的消息传输协议。它的设计思想是轻巧、开放、 简单、规范,易于实现。这些特点使得它对很多场景来说都是很好的选择,特别是对于受限的环境如机器与机器的通信(M2M)以及物联网环境(IoT

应用于,车联网,智能家居;

MQTT特点:

订阅模式

账号A,发布蔬菜管理B主题、和蛋白质管理C、

账号F,只能读取到订阅的B主题,就算A发布了C,没有订阅就不会收到

主题机制;类似于can总线的筛选机制;屏蔽非订阅的主题

MQTT特点

消息模式

发布/订阅消息模式,提供一对多的消息发布

可靠传输

MQTT 是基于 TCP 连接进行数据推送的

服务等级

支持 QoS 等级。根据消息的重要性不同设置不同的服务等级

轻量级

小型传输,开销很小,协议交换最小化,以降低网络流量

遗嘱机制

使用 will 遗嘱机制来通知客户端异常断线

主题机制

基于主题发布/订阅消息,对负载内容屏蔽的消息传输

MQTT一共两个版本

MQTT5 是在 MQTT3.1.1 的基础上进行了升级,因此 MQTT5 是完全兼容 MQTT3.1.1

服务端(broker):它是MQTT 信息传输的枢纽,负责数据传递和客户端管理,确保客户端之间通讯顺畅。

客户端(Client):“发布” :向服务器发布信息;“订阅”:从服务器收取信息

客户端可以是订阅者也可以是发布者!!!

主机/服务器架构关系图

客户端的发布和订阅都是围绕着“主题”来进行的!!!

客户端订阅的主题才能被被收到

MQTT发布/订阅特性

相互独立:MQTT客户端相互独立,依然可以实现信息交流。

空间分离:MQTT客户端和MQTT服务端处于同一个通信网络中。

时间异步:MQTT 客户端在发送和接收信息时无需同步。

QoS服务质量

服务质量是 MQTT 的一个重要特性。当我们使用 TCP/IP 时,连接已经在一定程度上受到保护。但是在无线网络中,中断和干扰很频繁,MQTT 在这里帮助避免信息丢失及其服务质量水平。这些级别在发布时使用。

服务质量事实就是在表示报文要发布几次

QOS 0:最多一次,即:<=1         要么发一次,要么不发

QOS 1:至少一次,即:>=1         直到你收到

QOS 2:一次,即:=1                   就发一次,管你收没收到

QoS0服务质量

发送端一旦发送完消息后,就完成任务了。发送端不会检查发出的消息能否被正确接收到

QoS1服务质量

QoS级别为1时,发送端在消息发送完成后,会检查接收端是否已经成功接收到了消息

QoS2服务质量

发送端需要接收端进行两次消息确认。因此,2MQTT服务质量是最安全的服务级别,也是最慢的服务级别

MQTT协议报文结构

必选

固定报头(Fixed header),存在于所有MQTT数据包中,表示数据包类型及数据包的分组类标识

固定报头理论

上至少2个字节

可选

可变报头(Variable header),存在于部分MQTT数据包中,数据包类型决定了

可选

有效负载(Payload),存在于部分MQTT数据包中,表示客户端收到的具体内

 固定报头结构

固定报头至少2个字节,第1个字节:

4位:报文类型    影响剩余长度的内容(可变报头/可变负载

4位:与报文类型相关的标志位

2个字节及之后的字节(至多4个字节)是剩余数据的长度

固定报头之报文类型

名字

报文流动方向

描述

Reserved

0

禁止

保留位

CONNECT

1

客户端à服务端

客户端请求连接服务端

CONNACK

2

服务端à客户端

服务端连接报文确认

PUBLISH

3

两个方向都允许

发布消息

PUBACK

4

发布确认(QoS 1)

PUBREC

5

发布收到(保证交付第一步, QoS 2

PUBREL

6

发布释放(保证交付第二步, QoS 2

PUBCOMP

7

发布完成(保证交互第三步, QoS 2

SUBSCRIBE

8

客户端à服务端

客户端订阅请求

SUBACK

9

服务端à客户端

服务端订阅确认

UNSUBSCRIBE

10

客户端à服务端

客户端取消订阅请求

UNSUBACK

11

服务端à客户端

服务端取消订阅确认

PINGREQ

12

客户端à服务端

客户端心跳请求

PINGRESP

13

服务端à客户端

服务端心跳响应

DISCONNECT

14

客户端à服务端

客户端断开连接

Reserved

15

禁止

保留位

固定报头报文类型标志位

控制报文

固定报头

标志

bit 3

bit 2

bit 1

bit 0

CONNECT

Reserved

0

0

0

0

CONNACK

PUBLISH

Used in MQTT 3.1.1

DUP

 QoS

RETAIN

PUBACK

Reserved

0

0

0

0

PUBREC

PUBREL

0

0

1

0

PUBCOMP

0

0

0

0

SUBSCRIBE

0

0

1

0

SUBACK

0

0

0

0

UNSUBSCRIBE

0

0

1

0

UNSUBACK

0

0

0

0

PINGREQ

PINGRESP

DISCONNECT

PUBLISH这项,在MQTT3.1.1版本中用到了3种标志:

DUPPUBLISH报文的重复传送标志;

QoSPUBLISH报文的服务质量等级;

RETAINPUBLISH报文的保留标志。

DUP

0:客户端/服务器第一次尝试发送此 PUBLISH 数据包;

1:可能是对之前尝试发送数据包的重新发送。

当客户端或服务器尝试重新传递 PUBLISH 数据包时,DUP必须是1

对于所有 QoS 0 消息,DUP必须是0

MQTT可变头

某些类型的 MQTT 控制数据包包含可变报头,它位于固定报头和有效负载之间。可变报头的内容根据报文类型的不同而不同。

例如,CONNECT报文,它的可变报头有:协议名称(Protocol Name)、协议等级(Protocol Level)、连接标志(Connect Flags)

和 保活间隔(Keep Alive)等部分。可变报头的 报文标识符(Packet Identifier) 在几种数据包类型中是通用的:

控制报文

报文标识符  字段

CONNECT

不需要

CONNACK

PUBLISH

需要(如果 QoS > 0

PUBACK

需要

PUBREC

PUBREL

PUBCOMP

SUBSCRIBE

SUBACK

UNSUBSCRIBE

UNSUBACK

PINGREQ

不需要

PINGRESP

DISCONNECT

有效负载(Payload)

有些MQTT控制报文在数据包的最后部分包含有效负载,如下表所示,例如,对于PUBLISH报文来说,

有效负载就可以是应用消息。

控制报文

有效负载

CONNECT

需要

CONNACK

不需要

PUBLISH

可选

PUBACK

不需要

PUBREC

PUBREL

PUBCOMP

SUBSCRIBE

需要

SUBACK

UNSUBSCRIBE

UNSUBACK

不需要

PINGREQ

PINGRESP

DISCONNECT

包含CONNECTSUBSCRIBESUBACKUNSUBSCRIBE四种类型的消息:

1CONNECT,消息体内容主要是:客户端的ID(ClientID)、订阅的

TopicMessage以及用户名(账号)和密码。

2SUBSCRIBE,消息体内容是一系列的要订阅的Topic以及QoS

3SUBACK,消息体内容是服务器对于SUBSCRIBE所申请的Topic

QoS进行确认和回复。

4UNSUBSCRIBE,消息体内容是要取消订阅的Topic

MQTT协议原理解析

客户端与代理服务器建立连接

 首先要知道服务器地址和端口号

客户端向代理服务器订阅

客户端向代理服务器发布主题

onenet实验

1、使用onenet创建服务端:

网址:

OneNET - 中国移动物联网开放平台

2、使用PC模拟开发板作为客户端

onenet简要使用

1、开发者中心

2、产品开发

创建产品:主打随便

3、设备管理

添加一个设备:主打两个随便

最终获取到ID 、和密钥、   ip 、端口号

获取token工具

OneNET - 中国移动物联网开放平台

【免费】2025老版本的token软件资源-优快云文库

生成内容:核心密钥

res:填入内容

products/{pid}/devices/{device_name}

et:填入内容 时间戳单位秒

1672735919最高位加1 =2672735919  绝对错不了,大于当前时间秒数

可以百度在线时间戳,查看当前时间戳秒数;保证填入数据一定大于当前时间戳


key:填入内容

mothod:填入内容

md5

pid:获取

device_name:获取

核心密钥的作用:

通过connet,可以连接到对应的服务器,并且包含了心跳、周期等内容

oneos源码下载:

下载中心

oneos文件包下:

\components\cloud\onenet\m qtt-kit\authorization  文件
 

keil工程添加如下:

打开工程并在 Middlewares/lwip/lwip_app 分组

onenet开发手册

topic:翻译为主题

OneNET - 中国移动物联网开放平台

打开 OneNET 在线开发指南

https://open.iot.10086.cn/doc/v5/fuse/detail/919   

获取服务器地址和端口号

上传数据点

  • 文档中心 /MQTT物联网套件(新版) /最佳实践 /上传数据点

订阅和发布两种字符串方式

订阅上传结果通知消息

topic 命名规则如下:
$sys/{pid}/{device-name}/dp/post/json/+

设备数据点上传

topic 命名规则如下:
$sys/{pid}/{device-name}/dp/post/json

topic 簇

数据点 topic 簇

MQTT物联网套件支持用户以数据流-数据点模型(模型详情)将数据上传至平台并进行存储,设备可以通过数据点 topic 簇调用数据点存储服务存储数据,可以通过订阅系统 topic 获取数据处理结果通知

系统topic用途QoS可订阅可发布
$sys/{pid}/{device-name}/dp/post/json设备上传数据点0/1
$sys/{pid}/{device-name}/dp/post/json/accepted系统通知"设备上传数据点成功"0
$sys/{pid}/{device-name}/dp/post/json/rejected系统通知"设备上传数据点失败"0

设备命令topic簇

MQTT物联网套件支持应用通过API直接向设备发送单播命令,设备可以通过设备命令 topic 簇获取消息并进行消息应答

系统topic用途QoS可订阅可发布
$sys/{pid}/{device-name}/cmd/request/{cmdid}系统向设备下发命令0
$sys/{pid}/{device-name}/cmd/response/{cmdid}设备回复命令应答0/1
$sys/{pid}/{device-name}/cmd/response/{cmdid}/accepted系统回复"设备命令应答成功"0
$sys/{pid}/{device-name}/cmd/response/{cmdid}/rejected系统回复"设备命令应答失败"0

应用层的MQTT驱动库

下载地址

Software

onenet作为服务器下发发送数据

onenet实验主要实现代码


/* oneNET参考文章:https://open.iot.10086.cn/doc/v5/develop/detail/251 */

//static const struct mqtt_connect_client_info_t mqtt_client_info =
//{
//    "MQTT",     /* 设备名 */
//    "366007",   /* 产品ID */
//    "version=2018-10-31&res=products%2F366007%2Fdevices%2FMQTT&et=1672735919&method=md5&sign=qI0pgDJnICGoPdhNi%2BHtfg%3D%3D", /* pass */
//    100,  /* keep alive */
//    NULL, /* will_topic */
//    NULL, /* will_msg */
//    0,    /* will_qos */
//    0     /* will_retain */
//#if LWIP_ALTCP && LWIP_ALTCP_TLS  /* 加密操作,我们一般不使用加密操作 */
//  , NULL
//#endif
//};

static ip_addr_t g_mqtt_ip;
static mqtt_client_t* g_mqtt_client;
float g_temp = 0;    /* 温度值 */
float g_humid = 0;   /* 湿度值 */
unsigned char g_payload_out[200];
int g_payload_out_len = 0;
int g_publish_flag = 0;/* 发布成功标志位 */


/**
 * @brief       mqtt进入数据回调函数
 * @param       arg:传入的参数
 * @param       data:数据
 * @param       len:数据大小
 * @param       flags:标志
 * @retval      无
 */
static void 
mqtt_incoming_data_cb(void *arg, const u8_t *data, u16_t len, u8_t flags)
{
    printf("\r\ndata cb: len %d, flags %d\n",(int)len, (int)flags);
}

/**
 * @brief       mqtt进入发布回调函数
 * @param       arg:传入的参数
 * @param       topic:主题
 * @param       tot_len:主题大小
 * @retval      无
 */
static void
mqtt_incoming_publish_cb(void *arg, const char *topic, u32_t tot_len)
{
    printf("\r\npublish cb: topic %s\r\n",topic);
    printf("\r\nlen %d\r\n",(int)tot_len);
}

/**
 * @brief       mqtt发布回调函数
 * @param       arg:传入的参数
 * @param       err:错误值
 * @retval      无
 */
static void
mqtt_publish_request_cb(void *arg, err_t err)
{
    printf("publish success\r\n");
}

/**
 * @brief       mqtt订阅响应回调函数
 * @param       arg:传入的参数
 * @param       err:错误值
 * @retval      无
 */
static void
mqtt_request_cb(void *arg, err_t err)
{
    g_publish_flag = 1;
    printf("\r\nrequest cb: err %d\r\n",(int)err);
}

/**
 * @brief       mqtt连接回调函数
 * @param       client:客户端控制块
 * @param       arg:传入的参数
 * @param       status:连接状态
 * @retval      无
 */
static void
mqtt_connection_cb(mqtt_client_t *client, void *arg, mqtt_connection_status_t status)
{
    err_t err;
    
    const struct mqtt_connect_client_info_t* client_info = (const struct mqtt_connect_client_info_t*)arg;
    
    LWIP_UNUSED_ARG(client);
    
    taskENTER_CRITICAL();           /* 进入临界区 */
    printf("\r\nMQTT client \"%s\" connection cb: status %d\r\n", client_info->client_id, (int)status);
    taskEXIT_CRITICAL();            /* 退出临界区 */
    
    /* 判断是否连接 */ 
    if (status == MQTT_CONNECT_ACCEPTED)
    {
        /* 判断是否连接 */ 
        if (mqtt_client_is_connected(client))
        {
            /* 设置传入发布请求的回调 */
            mqtt_set_inpub_callback(g_mqtt_client,
                                    mqtt_incoming_publish_cb,
                                    mqtt_incoming_data_cb,
                                    NULL);
            
            /* 订阅操作,并设置订阅响应会回调函数mqtt_sub_request_cb */ 
            err = mqtt_subscribe(client, DEVICE_SUBSCRIBE, 1, mqtt_request_cb, arg);
            
            if(err == ERR_OK)
            {
                printf("mqtt_subscribe return: %d\n", err);
                lcd_show_string(5, 170, 210, 16, 16, "mqtt_subscribe succeed", BLUE);
            }
            
            /* 订阅服务器下发的命令 */ 
            err = mqtt_subscribe(client, SERVER_PUBLISH, 1, mqtt_request_cb, arg);
            
            /* 判断是否订阅成功 */ 
            if(err == ERR_OK)
            {
                lcd_show_string(5, 190, 210, 16, 16, "mqtt_subscribe cmd succeed", BLUE);
            }
        }
    }
    else/* 连接失败 */
    {
        printf("mqtt_connection_cb: Disconnected, reason: %d\n", status);
    } 
}

/**
 * @brief       lwip_demo进程
 * @param       无
 * @retval      无
 */
void lwip_demo(void)
{
    static struct mqtt_connect_client_info_t mqtt_client_info;
    struct hostent *server;
    
    char version[]  = "2018-10-31";
    unsigned int expiration_time    = 1956499200;
    char authorization_buf[160] = {0};
    
    server = gethostbyname((char *)HOST_NAME);          /* 对oneNET服务器地址解析 */
    memcpy(&g_mqtt_ip,server->h_addr,server->h_length); /* 把解析好的地址存放在mqtt_ip变量当中 */
    
    /* 设置一个空的客户端信息结构 */
    memset(&mqtt_client_info, 0, sizeof(mqtt_client_info));
    
    /* 根据这些参数进行解码,当然这个密码可以在token软件下解码 */
    onenet_authorization(version,
                         (char *)USER_PRODUCT_ID,
                         expiration_time,
                         (char *)USER_KEY,
                         (char *)USER_DEVICE_NAME,
                         authorization_buf,
                         sizeof(authorization_buf),
                         0);
    
    /* 设置客户端的信息量 */ 
    mqtt_client_info.client_id = (char *)USER_DEVICE_NAME;      /* 设备名称 */
    mqtt_client_info.client_user = (char *)USER_PRODUCT_ID;     /* 产品ID */
    mqtt_client_info.client_pass = (char *)authorization_buf;   /* 计算出来的密码 */
    mqtt_client_info.keep_alive = 100;                          /* 保活时间 */
    mqtt_client_info.will_msg = NULL;
    mqtt_client_info.will_qos = NULL;
    mqtt_client_info.will_retain = 0;
    mqtt_client_info.will_topic = 0;

    /* 创建MQTT客户端控制块 */
    g_mqtt_client = mqtt_client_new();
    
    /* 连接服务器 */
    mqtt_client_connect(g_mqtt_client,        /* 服务器控制块 */
                        &g_mqtt_ip, MQTT_PORT,/* 服务器IP与端口号 */
                        mqtt_connection_cb, LWIP_CONST_CAST(void*, &mqtt_client_info),/* 设置服务器连接回调函数 */
                        &mqtt_client_info);   /* MQTT连接信息 */
    while(1)
    {
        if (g_publish_flag == 1)
        {
            g_temp = 30 + rand() % 10 + 1;   /* 温度的数据 */
            g_humid = 54.8 + rand() % 10 + 1;/* 湿度的数据 */
            sprintf((char *)g_payload_out, "{\"id\": 123,\"dp\": { \"temperatrue\": [{\"v\": %0.1f,}],\"power\": [{\"v\": %0.1f,}]}}", g_temp, g_humid);
            g_payload_out_len = strlen((char *)g_payload_out);
            mqtt_publish(g_mqtt_client,DEVICE_PUBLISH,g_payload_out,g_payload_out_len,1,0,mqtt_publish_request_cb,NULL);
        }
        
        vTaskDelay(1000);
    }
}

原子云实验

开发板为客户端

原子云为服务器

使用原子云的信息

服务器地址:cloud.alientek.com  域名可以在线工具解析为ip

开发端口:59666

创建设备:收发数据的终端

添加正点原子的库文件替代mqtt库

 添加相关正点原子提供的     .lib文件

原子云操作

原子云

设备管理

取得编号

和密码

主实现代码


#define LWIP_SEND_THREAD_PRIO  ( tskIDLE_PRIORITY + 3 )
#define LWIP_DEMO_RX_BUFSIZE         100      /* 最大接收数据长度 */
int g_mysock;                                 /* 发送scoker */
int g_rc = 0;                                 /* 是否发送数据成功标志位 */
/* 接收数据缓冲区 */
uint8_t g_lwip_demo_recvbuf[LWIP_DEMO_RX_BUFSIZE];  
/* 发送数据内容 */
uint8_t g_lwip_demo_sendbuf[] = "ALIENTEK DATA \r\n";
/* 数据发送标志位 */
uint8_t g_lwip_send_flag;

/**
* @brief  通过TCP方式发送数据到TCP服务器
* @param  sock:接口
* @param  buf数据首地址
* @param  buflen数据长度
* @retval 小于0表示发送失败
*/
int lwip_transport_send_packet_buffer(int sock, unsigned char *buf, int buflen)
{
    int rc = 0;
    rc = write(sock, buf, buflen);
    return rc;
}

/**
* @brief  阻塞方式接受TCP服务器发送的数据
* @param  sock:接口
* @param  buf数据存储首地址
* @param  count数据缓冲区长度
* @retval 小于0表示接收数据失败
*/
int lwip_transport_getdata(uint8_t* buf, int32_t count)
{
    int rc = recv(g_mysock, buf, count, 0);
    return rc;
}

/**
* @brief  打开一个网络接口,其实就是和服务器建立一个 TCP 连接。
* @param  addr:地址
* @param  port:端口
* @retval 小于0表示接收数据失败
*/
int lwip_transport_open(char* addr, int port)
{
    int* sock = &g_mysock;
    struct hostent *server;
    struct sockaddr_in serv_addr;
    int timeout = 1000;

    *sock = socket(AF_INET, SOCK_STREAM, 0);
    if(*sock < 0)
      printf("[ERROR] Create socket failed\n");
    
    server = gethostbyname(addr);              /* 对阿里云服务器地址解析 */
    if(server == NULL)
      printf("[ERROR] Get host ip failed\n");
    
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(port);          /* 设置端口号 */
    memcpy(&serv_addr.sin_addr.s_addr,server->h_addr,server->h_length);
    
    if(connect(*sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0)
    {
      printf("[ERROR] connect failed\n");
          return -1;
    }
    
    setsockopt(g_mysock, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout,sizeof(timeout));

    return g_mysock;
}

/**
* @brief  关闭连接
* @param  sock:socket控制块
* @retval 返回关闭连接消息
*/
int lwip_transport_close(int sock)
{
    int rc;

    rc = shutdown(sock, SHUT_WR);
    rc = recv(sock, NULL, (size_t)0, 0);
    rc = close(sock);

    return rc;
}

static void lwip_send_thread(void *arg);

/**
 * @brief       发送数据线程
 * @param       无
 * @retval      无
 */
void lwip_data_send(void)
{
    sys_thread_new("lwip_send_thread", lwip_send_thread, NULL, 512, LWIP_SEND_THREAD_PRIO );
}

/**
* @brief  lwip_demo程序入口
* @param  无
* @retval 无
*/
void lwip_demo(void)
{
    int err;
    
    g_mysock = lwip_transport_open(HOST_NAME,HOST_PORT);
    err = lwip_transport_send_packet_buffer(g_mysock,
                                           (uint8_t *)atk_decode("61259083526781695524","12345678"), //原子云的,设备编号和密码
                                            strlen((char *)atk_decode("61259083526781695524","12345678")));
    
    if (err > 0)
    {
        lcd_show_string(5, 170, 200, 16, 16, "Link succeed", BLUE);
        lwip_data_send(); /* 创建发送线程 */

        while (1)
        {
            memset(g_lwip_demo_recvbuf, 0, sizeof(g_lwip_demo_recvbuf));
            g_rc = lwip_transport_getdata(g_lwip_demo_recvbuf,sizeof(g_lwip_demo_recvbuf));
            
            if (g_rc > 0)
            {
                lcd_fill(5, 210,lcddev.width,lcddev.height,WHITE);
                lcd_show_string(5, 210, lcddev.width, 16, 16, (char *)g_lwip_demo_recvbuf, BLUE);
            }
            
            vTaskDelay(10);
        }
    }
    
    lcd_show_string(5, 170, 200, 16, 16, " Link fail", RED);
}

/**
 * @brief       发送数据线程函数
 * @param       pvParameters : 传入参数(未用到)
 * @retval      无
 */
static void lwip_send_thread(void *pvParameters)
{
    pvParameters = pvParameters;
    
    while (1)
    {
        if ((g_lwip_send_flag & LWIP_SEND_DATA) == LWIP_SEND_DATA) /*有数据要发送*/
        {
            lwip_transport_send_packet_buffer(g_mysock,(uint8_t *)g_lwip_demo_sendbuf,sizeof(g_lwip_demo_sendbuf)); /* 发送lwip_demo_sendbuf中的数据 */
            g_lwip_send_flag &= ~LWIP_SEND_DATA;
        }
        vTaskDelay(10);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值