1. MQTT简介
MQTT 协议是为工作在低带宽、不可靠网络的远程传感器和控制设备之间的通讯而设计的协议,它具有以下主要的几项特性:
- 使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。
- 基于 TCP/IP 提供网络连接。
- 支持 QoS 服务质量等级。
- 小型传输, 开销很小,协议交换最小化,以降低网络流量。
- 使用 will 遗嘱机制来通知客户端异常断线
- 基于主题发布/订阅消息,对负载内容屏蔽的消息传输。
- 支持心跳机制。
MQTT是一种基于客户端-服务端架构的消息传输协议,MQTT协议通信的大致流程如下图所示:
2. MQTT客户端库API介绍
在编写客户端程序时,需要了解MQTT客户端库提供的常用的API。
2.1 MQTTClient_message结构体
MQTT 客户端应用程序发布消息和接收消息都是围绕着这个结构体。 MQTTClient_message 数据结构描述了 MQTT 消息的负载和属性等相关信息,譬如消息的负载、负载的长度、 qos、消息的保留标志、 dup 标志等。该结构体内容如下:
typedef struct
{
int payloadlen; //负载长度
void* payload; //负载
int qos; //消息的 qos 等级
int retained; //消息的保留标志
int dup; //dup 标志(重复标志)
int msgid; //消息标识符
......
} MQTTClient_message;
2.2 创建一个客户端对象
在连接服务端之前,需要创建一个客户端对象,使用 MQTTClient_create 函数创建:
int MQTTClient_create(MQTTClient *handle,
const char *serverURI,
const char *clientId,
int persistence_type,
void *persistence_context
);
handle: MQTT 客户端句柄;
serverURL: MQTT 服务器地址;
clientId: 客户端 ID;
persistence_type: 客户端使用的持久化类型:
2.3 连接服务端
int MQTTClient_connect(MQTTClient handle,
MQTTClient_connectOptions *options
);
handle: 客户端句柄;
options: 一个指针。指向一个 MQTTClient_connectOptions 结构体对象。 MQTTClient_connectOptions 结构体中包含keepAlive、 cleanSession 以及一个指向 MQTTClient_willOptions 结构体对象的指针 will_opts;MQTTClient_willOptions 结构体包含了客户端遗嘱相关的信息, 遗嘱主题、遗嘱内容、遗嘱消息的 QoS 等级、遗嘱消息的保留标志等。
2.4 调用回调函数
调用 MQTTClient_setCallbacks 函数为应用程序设置回调函数, MQTTClient_setCallbacks 可设置多个回调函数,包括:
-
断开连接时的回调函数 cl (当客户端检测到自己掉线时会执行该函数,如果将其设置为 NULL表示应用程序不处理断线的情况) 、
-
接收消息的回调函数 ma(当客户端接收到服务端发送过来的消息时执行该函数,必须设置此函数否则客户端无法接收消息)、
-
发布消息的回调函数 dc(当客户端发布的消息已经确认发送时执行该回调函数,如果你的应用程序采用同步方式发布消息或者您不想检查是否成功发送时,您可以将此设置为 NULL) 。
int MQTTClient_setCallbacks(MQTTClient handle,
void *context,
MQTTClient_connectionLost *cl,
MQTTClient_messageArrived *ma,
MQTTClient_deliveryComplete *dc
);
handle: 客户端句柄;
context: 执行回调函数的时候,会将 context 参数传递给回调函数,因为每一个回调函数都设置了一个参数用来接收 context 参数。
cl: 一个 MQTTClient_connectionLost 类型的函数指针,
ma: 一个 MQTTClient_messageArrived 类型的函数指针
dc: 一个 MQTTClient_deliveryComplete 类型的函数指针
注意:调用 MQTTClient_setCallbacks 函数设置回调必须在连接服务器之前完成!
2.5 发布消息
当客户端成功连接到服务端之后,便可以发布消息或订阅主题了,应用程序通过MQTTClient_publishMessage 库函数来发布一个消息:
int MQTTClient_publishMessage(MQTTClient handle,
const char *topicName,
MQTTClient_message *msg,
MQTTClient_deliveryToken *dt
);
handle: 客户端句柄;
topicName: 主题名称。向该主题发布消息。
msg: 指向一个 MQTTClient_message 对象的指针。
dt: 返回给应用程序的传递令牌。
返回值: 成功返回 MQTTCLIENT_SUCCESS,失败返回错误码。
2.6 订阅主题和取消订阅主题
客户端应用程序调用 MQTTClient_subscribe 函数来订阅主题:
int MQTTClient_subscribe(MQTTClient handle,
const char *topic,
int qos
);
handle: 客户端句柄;
topic: 主题名称。客户端订阅的主题。
qos: QoS 等级。
返回值: 成功返回 MQTTCLIENT_SUCCESS,失败返回错误码 。
当客户端想取消之前订阅的主题时,可调用 MQTTClient_unsubscribe 函数,如下所示:
int MQTTClient_unsubscribe(MQTTClient handle,
const char *topic
);
handle: 客户端句柄;
topic: 主题名称。取消订阅该主题。
返回值: 成功返回 MQTTCLIENT_SUCCESS,失败返回错误码。
2.7 断开服务器连接
当客户端需要主动断开与客户端连接时,可调用 MQTTClient_disconnect 函数:
int MQTTClient_disconnect(MQTTClient handle,
int timeout
);
handle: 客户端句柄;
timeout: 超时时间。 客户端将断开连接延迟最多 timeout 时间(以毫秒为单位),以便完成正在进行中的消息传输。
返回值: 如果客户端成功从服务器断开连接,则返回 MQTTCLIENT_SUCCESS; 如果客户端无法与服务器断开连接,则返回错误代码。
3. 编写客户端程序
3.1功能设计
- 基于公共MQTT服务器实现个人物联网小项目
- 用户可通过手机或电脑远程控制Linux开发板上的外设,譬如LED、蜂鸣器
- 开发板客户端每隔一段时间向服务端发送当前开发板的运行状态信息,用户可从服务端远程获取该信息
3.2 客户端应用程序源文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "MQTTClient.h"
#define BROKER_ADDRESS "......" //公共MQTT服务器地址
#define CLIENTID "......" //客户端id
// #define USERNAME "" //用户名
// #define PASSWORD "" //密码
#define WILL_TOPIC "mqtt/will" //遗嘱主题
#define LED_TOPIC "mqtt/led" LED主题
#define TEMP_TOPIC "mqtt/temperature" //温度主题
/* 接收消息的回调函数 */
static int msgarrvd(void *context,char *topicName,int topicLen,MQTTClient_message *message)
{
if(!strcmp(topicName,LED_TOPIC)){ //校验消息的主题
if(!strcmp("2",message->payload)) //如果接收到的消息是"2"则设置LED为呼吸灯模式
system("echo heartbeat > /sys/class/leds/sys-led/trigger");
if(!strcmp("1",message->payload)){ //如果是"1"则LED常量
system("echo none > /sys/class/leds/sys-led/trigger");
system("echo 1 > /sys/class/leds/sys-led/brightness");
}
else if(!strcmp("0",message->payload)){ //如果是"0"则LED熄灭
system("echo none > /sys/class/leds/sys-led/trigger");
system("echo 0 > /sys/class/leds/sys-led/brightness");
}
// 接收到其它数据 不做处理
}
/* 释放占用的内存空间 */
MQTTClient_freeMessage(&message);
MQTTClient_free(topicName);
return 1;
}
/* 断开连接时的回调函数 */
static void connlost(void *context, char *cause)
{
printf("\nConnection lost\n");
printf(" cause:%s\n",cause);
}
int main(int argc,char *argv[])
{
MQTTClient client;
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer;
MQTTClient_message pubmsg = MQTTClient_message_initializer;
int rc;
/* 创建mqtt客户端对象 */
if(MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_create(&client,BROKER_ADDRESS,CLIENTID,MQTTCLIENT_PERSISTENCE_NONE,NULL))){
printf("Failed to create client,return code %d\n",rc);
rc = EXIT_FAILURE;
goto exit;
}
/* 设置回调 */
if(MQTTCLIENT_SUCCESS != (rc = MQTTClient_setCallbacks(client,NULL,connlost,msgarrvd,NULL))){
printf("Failed to set callbacks,return code %d\n",rc);
rc = EXIT_FAILURE;
goto destory_exit;
}
/* 连接MQTT服务器 */
will_opts.topicName = WILL_TOPIC; //遗嘱主题
will_opts.message = "Unexpected disconnection";//遗嘱消息
will_opts.retained = 1; //保留消息
will_opts.qos = 0; //QoS0
conn_opts.will = &will_opts;
conn_opts.keepAliveInterval = 30; //心跳包间隔时间
conn_opts.cleansession = 0; //cleanSession标志
// conn_opts.username = USERNAME; //用户名
// conn_opts.password = PASSWORD; //密码
if(MQTTCLIENT_SUCCESS != (rc = MQTTClient_connect(client,&conn_opts))){
printf("Failed to connect,return code %d\n",rc);
rc = EXIT_FAILURE;
goto destory_exit;
}
printf("MQTT connect success!\n");
/* 发布上线消息 */
pubmsg.payload = "Online"; //消息的内容
pubmsg.payloadlen = 6; //内容的长度
pubmsg.qos = 0; //QoS等级
pubmsg.retained = 1; //保留消息
if(MQTTCLIENT_SUCCESS != (rc = MQTTClient_publishMessage(client,WILL_TOPIC,&pubmsg,NULL))){
printf("Failed to publish message,return code %d\n",rc);
rc = EXIT_FAILURE;
goto disconnect_exit;
}
/* 订阅主题 dt_mqtt/led */
if(MQTTCLIENT_SUCCESS != (rc = MQTTClient_subscribe(client,LED_TOPIC,0))){
printf("Failed to subscribe,return code %d\n",rc);
rc = EXIT_FAILURE;
goto disconnect_exit;
}
/* 向服务端发布开发板运行状态信息 */
for(;;){
MQTTClient_message tempmsg = MQTTClient_message_initializer;
char temp_str[10] = {0x0};
int fd;
/* 读取温度值 */
fd = open("/sys/class/thermal/thermal_zone0/temp",O_RDONLY);
if(0 > fd){
perror("open error");
}
if(0 > read(fd,temp_str,sizeof(temp_str))){ //读取temp属性文件即可获取温度
perror("read error");
}
close(fd);
/* 发布温度信息 */
tempmsg.payload = temp_str; //消息的内容
tempmsg.payloadlen = strlen(temp_str); //内容的长度
tempmsg.qos = 0; //QoS等级
tempmsg.retained = 1; //保留消息
if(MQTTCLIENT_SUCCESS != (rc = MQTTClient_publishMessage(client,TEMP_TOPIC,&tempmsg,NULL))){
printf("Failed to publish message,return code %d\n",rc);
rc = EXIT_FAILURE;
goto unsubscribe_exit;
}
sleep(30); //每隔30秒 更新一次数据
}
unsubscribe_exit:
if(MQTTCLIENT_SUCCESS != (rc = MQTTClient_unsubscribe(client,LED_TOPIC))){
printf("Failed to unsubscribe,return code %d\n",rc);
rc = EXIT_FAILURE;
}
disconnect_exit:
if(MQTTCLIENT_SUCCESS != (rc = MQTTClient_disconnect(client,10000))){
printf("Failed to disconnect,return code %d\n",rc);
rc = EXIT_FAILURE;
}
destory_exit:
MQTTClient_destroy(&client);
exit:
return rc;
}
3.3 演示
在进行测试之前,确保开发板是能够上网的,也就是能够连接外网。之后执行mqttClient客户端应用程序,如下所示:
这样,我们的开发板作为 MQTT 客户端就成功连接上了 MQTT 服务器,连接服务器时还会向服务器发送一条上线信息,并且每隔 30 秒向服务器发布温度信息,同样也会接收 LED 主题的信息。 如果我们在电脑上的客户端订阅了will和temperature主题,就会收到如下图所示的信息:
接下来我们在客户端上发布LED主题,控制开发板上LED灯的亮灭:
发0让LED熄灭:
效果:
发1让LED点亮:
效果:
发2让LED处于呼吸灯模式:
当开发板上的客户端意外掉线,被动与服务器断开连接,服务端就会把该客户端写好的遗嘱发布给其他订阅了该遗嘱主题的客户端,如下图中最后一条消息所示: