前面几节讲的都是Mosquitto的服务器端和客户端,这节看看另外一个常用的MQTT客户端的开源项目paho mqtt,它还有其他的语言版本,这里主要是看C语言版本的项目。
下载地址: https://github.com/eclipse/paho.mqtt.c.git
unzip paho.mqtt.c-master.zip
# 或 git clone https://github.com/eclipse/paho.mqtt.c.git
cd paho.mqtt.c-master/
make
sudo make install # 这一步可以不用执行
执行到make
编译时,就已经生成了动态库文件,make install
只是将这些动态库和源码中的头文件拷贝进系统中。为了避免操作到系统的文件,我们暂不执行安装命令。(注意,在make时可能会出现缺乏openssl/ssl.h函数错误,执行sudo apt-get install libssl-dev
安装即可。)
book@Ubuntu:/work/system/paho.mqtt.c-master$ ls build/output/
libpaho-mqtt3a.so libpaho-mqtt3as.so.1.3 libpaho-mqtt3cs.so.1
libpaho-mqtt3a.so.1 libpaho-mqtt3c.so libpaho-mqtt3cs.so.1.3
libpaho-mqtt3a.so.1.3 libpaho-mqtt3c.so.1 paho_c_version
libpaho-mqtt3as.so libpaho-mqtt3c.so.1.3 samples
libpaho-mqtt3as.so.1 libpaho-mqtt3cs.so test
同步函数的执行也就是会阻塞等待某个函数调用完毕返回之后才会执行下一步的操作。那么问题就来了,如果这是一个可视化的窗口,而且网络不是特别好的时候,这时的缺点就暴露出来了,发生连接超时等情况很容易导致程序会一直冻结在那里,那么体验肯定不是特别好了。但它的优点就是程序结构比较简单。
程序开始就是根据给它创建一个实例,接着定义和配置用于连接服务器的结构体,连接上服务器之后就是订阅主题,然后程序就进入了循环阻塞等待接收。需要注意的是,客户端与服务器端通过ping来keepalive的时候也会导致MQTTClient_receive函数返回MQTTCLIENT_SUCCESS,所以就需要对传出的参数进行判断。
另外,同步方式中也支持设置callback回调函数来进行异步接收消息,以下列出这2种方式的通讯程序。除了这种设置回调函数的异步方式,paho.mqtt.c还提供了异步通信的专用函数,放在下一节再研究。
(订阅端——MQTTClient_receive阻塞等待方式)
#include
#include
#include
#include
#include "MQTTClient.h"
#define ADDRESS "tcp://127.0.0.1:2020"
#define CLIENTID "mysub"
#define TOPIC "test/+"
#define USERNAME "user_test"
#define PASSWD "123456"
#define QOS 2
#define KEEPALIVE 20
#define TIMEOUT 10000L
int main(int argc, char* argv[])
{
MQTTClient client;
MQTTClient_message* msg_opts = NULL;
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
int rc;
int topic_len;
char* topic = NULL;
// 创建MQTT实例,参数:传出句柄、服务器URI、客户端ID、机制(默认值即可)、传给机制的内容
if ((rc = MQTTClient_create(&client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL))
!= MQTTCLIENT_SUCCESS)
{
printf("Failed to create client, return code %d\n", rc);
return -1;
}
// 配置参数进行连接
conn_opts.keepAliveInterval = KEEPALIVE;
conn_opts.cleansession = 1;
conn_opts.username = USERNAME;
conn_opts.password = PASSWD;
if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to connect, return code %d\n", rc);
MQTTClient_destroy(&client);
return -1;
}
// 订阅主题,参数:句柄、主题、QoS质量
if ((rc = MQTTClient_subscribe(client, TOPIC, QOS)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to subscribe, return code %d\n", rc);
MQTTClient_destroy(&client);
return -1;
}
printf("The way of receive!\n");
while(1)
{
if ((rc = MQTTClient_receive(client, &topic, &topic_len, &msg_opts, TIMEOUT)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to receive, return code %d\n", rc);
MQTTClient_destroy(&client);
return -1;
}
// 注意:客户端与服务器端ping发送心跳包时也会返回MQTTCLIENT_SUCCESS
// 所以这里必须进行判断,否则程序会发生段错误
if(topic != NULL && msg_opts != NULL)
{
printf("Recieve a message: (topic)%s, (msg)%s\n", topic, (char*)msg_opts->payload);
if(0 == strcmp((char*)msg_opts->payload, "quit"))
{
break;
}
MQTTClient_freeMessage(&msg_opts);
MQTTClient_free(topic);
}
}
MQTTClient_unsubscribe(client, TOPIC); // 取消订阅
MQTTClient_disconnect(client, TIMEOUT); // 断开连接,超时单位ms
MQTTClient_destroy(&client); // 销毁实例
return 0;
}
(订阅端——MQTTClient_setCallbacks回调的异步方式)
#include
#include
#include
#include
#include "MQTTClient.h"
#define ADDRESS "tcp://127.0.0.1:2020"
#define CLIENTID "mysub"
#define TOPIC "test/+"
#define USERNAME "user_test"
#define PASSWD "123456"
#define QOS 2
#define KEEPALIVE 20
#define TIMEOUT 10000L
int g_iRunFlag = 1;
int msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message)
{
printf("Recieve a message: (topic)%s, (msg)%s\n", topicName, (char*)message->payload);
if(0 == strcmp(message->payload, "quit"))
{
g_iRunFlag = 0;
}
MQTTClient_freeMessage(&message);
MQTTClient_free(topicName);
return 1;
}
int main(int argc, char* argv[])
{
MQTTClient client;
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
int rc;
// 创建MQTT实例,参数:传出句柄、服务器URI、客户端ID、机制(默认值即可)、传给机制的内容
if ((rc = MQTTClient_create(&client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL))
!= MQTTCLIENT_SUCCESS)
{
printf("Failed to create client, return code %d\n", rc);
return -1;
}
// 设置回调函数,参数:句柄、传入内容、连接丢失的回调函数、消息到来的回调函数、传输完毕的回调函数
if ((rc = MQTTClient_setCallbacks(client, NULL, NULL, msgarrvd, NULL)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to set callbacks, return code %d\n", rc);
MQTTClient_destroy(&client);
return -1;
}
// 配置参数进行连接
conn_opts.keepAliveInterval = KEEPALIVE;
conn_opts.cleansession = 1;
conn_opts.username = USERNAME;
conn_opts.password = PASSWD;
if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to connect, return code %d\n", rc);
MQTTClient_destroy(&client);
return -1;
}
// 订阅主题,参数:句柄、主题、QoS质量
if ((rc = MQTTClient_subscribe(client, TOPIC, QOS)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to subscribe, return code %d\n", rc);
MQTTClient_destroy(&client);
return -1;
}
printf("The way of callback!\n");
while(g_iRunFlag)
{
// do something
sleep(2);
}
MQTTClient_unsubscribe(client, TOPIC); // 取消订阅
MQTTClient_disconnect(client, TIMEOUT); // 断开连接,超时单位ms
MQTTClient_destroy(&client); // 销毁实例
return 0;
}
发布端的结构也相对简单,在程序中设置了遗言消息,可以在程序运行期间按下Ctrl+c
给程序发送终止信号,让程序非正常退出,它的遗言消息就会被订阅端接收到。不仅仅是发布端,同为客户端的订阅端也可以设置遗言消息,但这里只是测试就没有设置。
#include
#include
#include
#include
#include "MQTTClient.h"
#define ADDRESS "tcp://127.0.0.1:2020"
#define CLIENTID "Rookie_pub"
#define USERNAME "user_test"
#define PASSWD "123456"
#define TOPIC "test/common"
#define PAYLOAD "hello"
#define WILL_TOPIC "test/will"
#define WILL_PAYLOAD "pub_will_message"
#define QOS 2
#define KEEPALIVE 20
#define TIMEOUT 10000L
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;
MQTTClient_deliveryToken token;
int rc;
int i;
// 创建MQTT实例,参数:句柄(传出)、服务器URI、客户端ID、机制(默认值即可)、传给机制的内容
if ((rc = MQTTClient_create(&client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL))
!= MQTTCLIENT_SUCCESS)
{
printf("Failed to create client, return code %d\n", rc);
return -1;
}
// 配置参数连接服务器
conn_opts.username = USERNAME;
conn_opts.password = PASSWD;
conn_opts.keepAliveInterval = KEEPALIVE;
conn_opts.cleansession = 1;
conn_opts.will = &will_opts;
will_opts.topicName = WILL_TOPIC;
will_opts.payload.data = WILL_PAYLOAD;
will_opts.payload.len = (int)strlen(WILL_PAYLOAD);
if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to connect, return code %d\n", rc);
MQTTClient_destroy(&client);
return -1;
}
// 构造消息的结构体
pubmsg.payload = PAYLOAD;
pubmsg.payloadlen = (int)strlen(PAYLOAD);
pubmsg.qos = QOS;
pubmsg.retained = 0;
for(i = 0; i < 2; i++)
{
// 发布消息,参数:句柄、主题、消息(结构体)、获得成功传输的数量(传出,可设为NULL)
if ((rc = MQTTClient_publishMessage(client, TOPIC, &pubmsg, &token)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to publish message, return code %d\n", rc);
MQTTClient_destroy(&client);
return -1;
}
// 等待发布完成,参数:句柄、成功传输的数量、允许的超时时长(ms)
if ((rc = MQTTClient_waitForCompletion(client, token, TIMEOUT)) != MQTTCLIENT_SUCCESS)
{
MQTTClient_destroy(&client);
return -1;
}
printf("%d messages have been published\n", token);
sleep(2);
}
MQTTClient_disconnect(client, TIMEOUT);
MQTTClient_destroy(&client);
return 0;
}
前面没有将这些库安装到系统目录中,编译时就需要手动指定头文件路径、库路径。
gcc 001mysub.c -o 001mysub -I/work/system/paho.mqtt.c-master/src/ -lpaho-mqtt3c -L/work/system/paho.mqtt.c-master/build/output/
gcc 001mypub.c -o 001mypub -I/work/system/paho.mqtt.c-master/src/ -lpaho-mqtt3c -L/work/system/paho.mqtt.c-master/build/output/
前面编译出来的程序时依赖于动态库才能运行,但这些动态库路径并不是在系统指定的标准目录中,所以还需要去指定动态库的路径。
export LD_LIBRARY_PATH=/work/system/paho.mqtt.c-master/build/output