MQTTClient API
MQTT 客户端库for C
基于C语言编写的MQTTClient API
,版权属于IBM,适用于2009年至2018年。
Paho MQTT C库中的API分为两类:
- 同步API(MQTTClient API)
- 特点:被认为是更易于使用,部分函数调用会导致程序暂停执行(即阻塞),直到操作完成。这意味着在等待操作结果时,程序不会继续执行其他任务。
- 线程安全:此API不是线程安全的,意味着在多线程环境下直接使用可能会遇到竞态条件等问题,需要开发者自己处理同步问题。
- 异步API(MQTTAsync API):
- 特点:设计为完全非阻塞的,所有调用都不会导致程序暂停,非常适合那些需要高度响应性的应用场景,特别是在有图形用户界面(GUI)的环境中,因为它能确保UI始终保持流畅。
- 线程安全:与同步API不同,异步API是线程安全的,意味着它可以在多线程程序中安全地使用,而不需要额外的同步措施。
本文章介绍的是MQTTClient API
。
MQTT客户端应用程序连接到支持MQTT的服务器。典型的客户端负责从遥测设备收集信息并将信息发布到服务器。它还可以订阅主题、接收消息,并使用这些信息控制遥测设备。
MQTT客户端实现已发布的MQTT v3协议。您可以使用自己选择的编程语言和平台为MQTT协议编写自己的API。这可能耗时且容易出错。
为了简化编写MQTT客户端应用程序,此库为您封装了MQTT v3协议。使用此库可以用几行代码编写功能齐全的MQTT客户端应用程序。这里提供的信息记录了MQTT客户端库为C提供的API。
使用MQTT客户端库开发应用程序
在使用MQTT客户端库开发应用程序时,通常会遵循以下结构化的步骤来组织代码逻辑:
- 创建客户端对象:首先,需要实例化一个MQTT客户端对象,这是进行MQTT通信的基础。
- 设置连接选项:接着,配置客户端连接至MQTT服务器所需的各种参数,比如服务器地址、端口、保持活动时间、用户名、密码等。
- 配置回调函数(可选):如果应用程序采用异步模式(多线程操作),需要定义并设置回调函数来处理网络事件、消息到达等异步行为。这些函数会在相应的事件发生时自动被调用。
- 订阅主题:根据应用需求,客户端需要订阅一个或多个主题,以便接收来自这些主题的消息更新。
- 消息循环处理:进入主循环,直到程序结束:
- 发布消息:根据应用逻辑,客户端可以向特定主题发布消息。
- 处理接收到的消息:在异步模式下,通过之前设置的回调函数处理接收到的消息;在同步模式下,则可能需要在循环中主动调用相关函数检查是否有新消息到达。
- 断开连接:当应用程序完成其任务后,应该优雅地断开与MQTT服务器的连接。
- 释放资源:最后,确保释放客户端对象及所有分配的资源,避免内存泄漏。
这样的流程确保了MQTT客户端应用能够有效地与MQTT服务器交互,无论是发送数据还是接收信息,同时保持了代码的清晰性和可维护性
示例
下面是对提到的几个简单示例及其涉及的重要概念的概述:
环境搭建和编译参考MQTT C Client for Posix and Windows
同步发布示例(Synchronous Publication Example)
在此类型的示例中,客户端使用同步方式发布消息到MQTT服务器。这意味着发布消息的函数会阻塞直到得到服务器的确认响应或者超时。这种模式适用于对实时性要求不高且发布消息后需要立即知道结果的场景。示例代码通常包括创建客户端、连接服务器、直接调用发布函数并等待响应、最后断开连接的过程。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "MQTTClient.h"
#define ADDRESS "tcp://localhost:1883"
#define CLIENTID "ExampleClientPub"
#define TOPIC "hellotopic/2"
#define PAYLOAD "Hello World!"
#define QOS 1
#define TIMEOUT 10000L
int main(int argc, char* argv[])
{
MQTTClient client;
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
MQTTClient_message pubmsg = MQTTClient_message_initializer;
MQTTClient_deliveryToken token;
int rc;
//1. 创建客户端对象
MQTTClient_create(&client, ADDRESS, CLIENTID,
MQTTCLIENT_PERSISTENCE_NONE, NULL);
conn_opts.keepAliveInterval = 20;
conn_opts.cleansession = 1;
//2.连接服务器
if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to connect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
pubmsg.payload = PAYLOAD;
pubmsg.payloadlen = strlen(PAYLOAD);
pubmsg.qos = QOS;
pubmsg.retained = 0;
//3.发布
MQTTClient_publishMessage(client, TOPIC, &pubmsg, &token);
printf("Waiting for up to %d seconds for publication of %s\n"
"on topic %s for client with ClientID: %s\n",
(int)(TIMEOUT/1000), PAYLOAD, TOPIC, CLIENTID);
rc = MQTTClient_waitForCompletion(client, token, TIMEOUT);
printf("Message with delivery token %d delivered\n", token);
//3.端口连接
MQTTClient_disconnect(client, 10000);
//4.释放资源
MQTTClient_destroy(&client);
return rc;
}
异步发布示例(Asynchronous Publication Example)
异步发布示例展示了如何在不阻塞主线程的情况下发布消息。客户端设置发布消息后,立即继续执行其他任务,而消息发送和响应处理则通过之前设定的回调函数异步完成。这种方式适用于需要高性能和响应性的应用,特别是在用户界面或实时数据处理场景中。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "MQTTClient.h"
#define ADDRESS "tcp://localhost:1883"
#define CLIENTID "ExampleClientPub"
#define TOPIC "hellotopic/3"
#define PAYLOAD "Hello World!"
#define QOS 1
#define TIMEOUT 10000L
volatile MQTTClient_deliveryToken deliveredtoken;
void delivered(void *context, MQTTClient_deliveryToken dt)
{
printf("Message with token value %d delivery confirmed\n", dt);
deliveredtoken = dt;
}
int msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message)
{
int i;
char* payloadptr;
printf("Message arrived\n");
printf(" topic: %s\n", topicName);
printf(" message: ");
payloadptr = message->payload;
for(i=0; i<message->payloadlen; i++)
{
putchar(*payloadptr++);
}
putchar('\n');
MQTTClient_freeMessage(&message);
MQTTClient_free(topicName);
return 1;
}
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_message pubmsg = MQTTClient_message_initializer;
MQTTClient_deliveryToken token;
int rc;
MQTTClient_create(&client, ADDRESS, CLIENTID,
MQTTCLIENT_PERSISTENCE_NONE, NULL);
conn_opts.keepAliveInterval = 20;
conn_opts.cleansession = 1;
MQTTClient_setCallbacks(client, NULL, connlost, msgarrvd, delivered);
if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to connect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
pubmsg.payload = PAYLOAD;
pubmsg.payloadlen = strlen(PAYLOAD);
pubmsg.qos = QOS;
pubmsg.retained = 0;
deliveredtoken = 0;
MQTTClient_publishMessage(client, TOPIC, &pubmsg, &token);
printf("Waiting for publication of %s\n"
"on topic %s for client with ClientID: %s\n",
PAYLOAD, TOPIC, CLIENTID);
while(deliveredtoken != token);
MQTTClient_disconnect(client, 10000);
MQTTClient_destroy(&client);
return rc;
}
异步订阅示例(Asynchronous Subscription Example)
在异步订阅示例中,客户端订阅主题后,当有新消息到达时,通过事先注册的回调函数自动处理这些消息。这允许程序在等待消息的同时执行其他任务,提高了效率和灵活性。示例通常涵盖设置回调、订阅主题、维持连接并在接收到消息时执行相应逻辑的步骤。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "MQTTClient.h"
#define ADDRESS "tcp://localhost:1883"
#define CLIENTID "ExampleClientSub"
#define TOPIC "hellotopic/#"
#define PAYLOAD "Hello World!"
#define QOS 1
#define TIMEOUT 10000L
volatile MQTTClient_deliveryToken deliveredtoken;
void delivered(void *context, MQTTClient_deliveryToken dt)
{
printf("Message with token value %d delivery confirmed\n", dt);
deliveredtoken = dt;
}
int msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message)
{
int i;
char* payloadptr;
printf("Message arrived\n");
printf(" topic: %s\n", topicName);
printf(" message: ");
payloadptr = message->payload;
for(i=0; i<message->payloadlen; i++)
{
putchar(*payloadptr++);
}
putchar('\n');
MQTTClient_freeMessage(&message);
MQTTClient_free(topicName);
return 1;
}
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;
int rc;
int ch;
MQTTClient_create(&client, ADDRESS, CLIENTID,
MQTTCLIENT_PERSISTENCE_NONE, NULL);
conn_opts.keepAliveInterval = 20;
conn_opts.cleansession = 1;
MQTTClient_setCallbacks(client, NULL, connlost, msgarrvd, delivered);
if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
{
printf("Failed to connect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
printf("Subscribing to topic %s\nfor client %s using QoS%d\n\n"
"Press Q<Enter> to quit\n\n", TOPIC, CLIENTID, QOS);
MQTTClient_subscribe(client, TOPIC, QOS);
do
{
ch = getchar();
} while(ch!='Q' && ch != 'q');
MQTTClient_disconnect(client, 10000);
MQTTClient_destroy(&client);
return rc;
}
重要概念补充说明
异步与同步客户端应用(Asynchronous vs Synchronous Client Applications)
MQTT客户端库支持两种操作模式:同步模式和异步模式。选择哪种模式主要取决于是否调用了MQTTClient_setCallbacks()函数。如果调用了这个函数,客户端就进入了异步模式;如果不调用,则默认为同步模式。
同步模式(Synchronous Mode)
- 单线程运行:在同步模式下,MQTT客户端应用程序在一个单独的线程上运行。
- 消息发布:使用MQTTClient_publish()或MQTTClient_publishMessage()函数发布消息。
- QoS1和QoS2消息的确认:为了确保QoS1或QoS2消息已成功传递,应用程序需要调用MQTTClient_waitForCompletion()函数来等待确认。具体查看
- 消息接收:在同步模式下,使用MQTTClient_receive()函数接收消息。客户端应用程序需要相对频繁地调用MQTTClient_receive()或MQTTClient_yield()函数,以允许处理确认和MQTT “pings”,这些"pings"用于保持与服务器的网络连接。
异步模式(Asynchronous Mode)
- 多线程运行:在异步模式下,MQTT客户端应用程序可以在多个线程上运行。
消息发布和订阅:与同步模式相同,主程序调用客户端库中的函数来发布和订阅消息。 - 后台处理:但是,握手(handshaking)和网络连接的维护是在后台进行的。
- 回调通知:客户端应用程序使用通过MQTTClient_setCallbacks()函数注册的回调函数来接收状态通知和消息接收通知。这些回调函数包括MQTTClient_messageArrived()(消息到达时调用)、MQTTClient_connectionLost()(连接丢失时调用)和MQTTClient_deliveryComplete()(消息传递完成时调用)。
- 线程安全:值得注意的是,虽然这种API在异步模式下提供了多线程功能,但它本身并不是线程安全的。也就是说,你不能在没有同步的情况下从多个线程调用这个API。如果你需要在多线程环境中使用MQTT客户端库,应该考虑使用MQTTAsync API,它提供了更好的线程安全性。
总结
选择同步模式还是异步模式取决于你的应用程序的需求。如果你的应用程序可以在单个线程上运行,并且你希望控制消息的发送和接收流程,那么同步模式可能是一个好的选择。然而,如果你的应用程序需要利用多线程来提高性能或响应性,并且你希望将网络连接的维护等任务交给库来处理,那么异步模式可能更适合你。
订阅通配符(Subscription Wildcards)
在MQTT通信中,每个消息都附有一个主题(topic
),用于对其进行分类。MQTT服务器依据这些主题决定将发布的消息推送给哪些订阅者。
假设服务器正在接收来自多个环境传感器的数据,每个传感器将其测量数据作为带有相应主题的消息发布。为了使订阅应用程序能够识别每条接收到的消息源自哪个传感器,就需要为每个传感器及其测量类型分配一个唯一主题。例如,可以使用SENSOR1TEMP
、SENSOR1HUMIDITY
、SENSOR2TEMP
等主题,但这种做法不够灵活。日后若系统中新增传感器,所有订阅应用程序都需要做出相应修改才能接收新传感器的数据。
为提升灵活性,MQTT支持层次化的主题命名空间。这让应用设计者能够组织管理主题,使之更为简便。层次结构中的各级别由/字符分隔,例如SENSOR/1/HUMIDITY
。发布者和订阅者均按照此层级主题进行操作。
在订阅时,支持两种通配符:
#
字符代表主题层级树的一个完整子树,因此必须作为订阅主题字符串的最后一个字符,如SENSOR/#
。这样可以匹配所有以SENSOR/
开头的主题,比如SENSOR/1/TEMP
和SENSOR/2/HUMIDITY
。+
字符代表层级结构中的单一级别,并置于分隔符之间。例如,SENSOR/+/TEMP
可以匹配SENSOR/1/TEMP和SENSOR/2/TEMP
。
需要注意的是,发布者在发布消息时不允许在其主题名称中使用通配符。
确定您的主题层次结构是系统设计中的一个重要步骤,因为它将影响消息的发布、订阅和路由方式。一个合理设计的主题层次结构可以提供灵活性,使系统能够轻松适应变化,并允许不同的应用程序订阅他们感兴趣的特定数据子集。
服务质量(Quality of Service, QoS)
MQTT协议为客户端和服务器间的消息传递提供了三种服务质量(Quality of Service, QoS)等级:“最多一次”、“至少一次"和"精确一次”。
服务质量是单个发布消息的属性,应用程序通过设置MQTTClient_message.qos字段为所需值来为特定消息指定QoS。订阅客户端还可以设置服务器用于发送与其订阅匹配的消息的最大服务质量,这可以通过MQTTClient_subscribe()和MQTTClient_subscribeMany()函数来完成。转发给订阅者的消息的服务质量可能会与原始发布者给予的消息QoS不同,实际采用的是两者中的较低值。
这三个等级具体如下:
- QoS0,最多一次(At most once):消息最多被送达一次,也可能根本不送达。其在网络中的传输不被确认,也不存储。如果客户端断开连接或服务器故障,消息可能会丢失。QoS0是最快的传输模式,有时称为"发送即忘"(fire and forget)模式。MQTT协议不要求服务器必须以QoS0转发消息给客户端,如果服务器收到发布信息时客户端已断开,根据服务器的具体实现,该消息可能被丢弃。
- QoS1,至少一次(At least once):消息至少被送达一次,如果有故障发生在确认接收前,消息可能会被多次送达。发送方必须将消息本地存储,直到收到接收方已发布消息的确认。存储消息是为了在必要时重新发送。
- QoS2,精确一次(Exactly once):消息总是被精确地送达一次。与QoS1类似,发送方也需要将消息本地存储,直至收到确认。QoS2是最安全但也是最慢的传输模式。相比于QoS1,它采用了更复杂的握手和确认序列来确保消息无重复。
跟踪(Tracing)
运行时跟踪功能可以通过环境变量来控制。
要开启跟踪功能,需要设置环境变量MQTT_C_CLIENT_TRACE
。将其值设为ON
或stdout
,则跟踪信息会被打印到标准输出(stdout)。如果设置为其他任何值,则该值会被解释为一个文件名,跟踪信息会被写入到这个文件中。
跟踪的详细程度可以通过环境变量MQTT_C_CLIENT_TRACE_LEVEL
来控制。有效的值包括ERROR
、PROTOCOL
、MINIMUM
、MEDIUM
和MAXIMUM
,这些值代表了从最少到最详细的日志级别。
另外,还有一个环境变量MQTT_C_CLIENT_TRACE_MAX_LINES
用于限制输出到文件的跟踪信息行数。最多使用两个文件进行轮换记录,当文件达到最大行数时,最新的跟踪记录会覆盖较早的文件内容。默认情况下,每个文件的最大行数是1000行。
MQTT数据包跟踪
MQTT数据包跟踪是一项非常实用的功能,它能够打印出发送和接收的MQTT数据包,便于开发者调试和监控通信过程。要启用这项功能,请设置以下环境变量:
MQTT_C_CLIENT_TRACE=ON
MQTT_C_CLIENT_TRACE_LEVEL=PROTOCOL
当你正确设置了MQTT数据包跟踪相关的环境变量后,你将在指定的输出位置(如终端或指定的文件)看到类似于以下格式的跟踪信息:
[timestamp] [Direction] [Packet Type] [Details]
20130528 155936.813 3 stdout-subscriber -> CONNECT cleansession: 1 (0)
20130528 155936.813 3 stdout-subscriber <- CONNACK rc: 0
20130528 155936.813 3 stdout-subscriber -> SUBSCRIBE msgid: 1 (0)
20130528 155936.813 3 stdout-subscriber <- SUBACK msgid: 1
20130528 155941.818 3 stdout-subscriber -> DISCONNECT (0)
上述示例中,每一行代表一个MQTT数据包的追踪记录,包含了时间戳、数据包的方向(-> from client to server, <- from server to client)、数据包类型(如CONNECT、CONNACK、SUBSCRIBE等)以及该数据包的具体详情,如客户端ID、清洁会话标志、保活时间、主题过滤器、服务质量等级(QoS)、消息载荷等。
这些信息对于理解客户端与MQTT服务器之间的交互过程,验证消息是否按预期发送和接收,以及解决网络或协议相关问题极为有用。
默认级别的跟踪
在默认级别的跟踪中,当你调用connect方法时,你可能会看到类似于以下内容的日志输出。请注意,具体的输出格式可能会根据不同的MQTT客户端库版本和实现有所不同,但核心信息大致相同:
19700101 010000.000 (1152206656) (0)> MQTTClient_connect:893
19700101 010000.000 (1152206656) (1)> MQTTClient_connectURI:716
20130528 160447.479 Connecting to serverURI localhost:1883
20130528 160447.479 (1152206656) (2)> MQTTProtocol_connect:98
20130528 160447.479 (1152206656) (3)> MQTTProtocol_addressPort:48
20130528 160447.479 (1152206656) (3)< MQTTProtocol_addressPort:73
20130528 160447.479 (1152206656) (3)> Socket_new:599
20130528 160447.479 New socket 4 for localhost, port 1883
20130528 160447.479 (1152206656) (4)> Socket_addSocket:163
20130528 160447.479 (1152206656) (5)> Socket_setnonblocking:73
20130528 160447.479 (1152206656) (5)< Socket_setnonblocking:78 (0)
20130528 160447.479 (1152206656) (4)< Socket_addSocket:176 (0)
20130528 160447.479 (1152206656) (4)> Socket_error:95
20130528 160447.479 (1152206656) (4)< Socket_error:104 (115)
20130528 160447.479 Connect pending
20130528 160447.479 (1152206656) (3)< Socket_new:683 (115)
20130528 160447.479 (1152206656) (2)< MQTTProtocol_connect:131 (115)
在这个例子中:
- date
- time
- thread id
- function nesting level
- function entry (>) or exit (<)
function name : line of source code file - return value (if there is one)
内存分配跟踪
当将跟踪级别设置为最大(MAXIMUM)时,除了默认的跟踪条目之外,还会追踪内存分配和释放的情况,生成类似于以下形式的信息:
20130528 161819.657 Allocating 16 bytes in heap at file /home/icraggs/workspaces/mqrtc/mqttv3c/src/MQTTPacket.c line 177 ptr 0x179f930
20130528 161819.657 Freeing 16 bytes in heap at file /home/icraggs/workspaces/mqrtc/mqttv3c/src/MQTTPacket.c line 201, heap use now 896 bytes
当最后一个MQTT客户端对象被销毁时,如果启用了跟踪记录并且客户端库分配的所有内存尚未被释放,将会有一条错误消息被写入跟踪记录中,这有助于发现和修复内存泄漏问题。这样的错误消息可能如下所示:
20130528 163909.208 Some memory not freed at shutdown, possible memory leak
20130528 163909.208 Heap scan start, total 880 bytes
20130528 163909.208 Heap element size 32, line 354, file /home/icraggs/workspaces/mqrtc/mqttv3c/src/MQTTPacket.c, ptr 0x260cb00
20130528 163909.208 Content
20130528 163909.209 Heap scan end