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服务质量
发送端需要接收端进行两次消息确认。因此,2级MQTT服务质量是最安全的服务级别,也是最慢的服务级别
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种标志:
DUP:PUBLISH报文的重复传送标志;
QoS:PUBLISH报文的服务质量等级;
RETAIN:PUBLISH报文的保留标志。
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 |
包含CONNECT、SUBSCRIBE、SUBACK、UNSUBSCRIBE四种类型的消息:
1)CONNECT,消息体内容主要是:客户端的ID(ClientID)、订阅的
Topic、Message以及用户名(账号)和密码。
2)SUBSCRIBE,消息体内容是一系列的要订阅的Topic以及QoS。
3)SUBACK,消息体内容是服务器对于SUBSCRIBE所申请的Topic
及QoS进行确认和回复。
4)UNSUBSCRIBE,消息体内容是要取消订阅的Topic。
MQTT协议原理解析
客户端与代理服务器建立连接
首先要知道服务器地址和端口号
客户端向代理服务器订阅
客户端向代理服务器发布主题
onenet实验
1、使用onenet创建服务端:
网址:
2、使用PC模拟开发板作为客户端
onenet简要使用
1、开发者中心
2、产品开发
创建产品:主打随便
3、设备管理
添加一个设备:主打两个随便
最终获取到ID 、和密钥、 ip 、端口号
获取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 在线开发指南
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驱动库
下载地址
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);
}
}