MQTT-C 简介
MQTT-C 是用 C 语言编写的 MQTT v3.1.1 客户端, 适用于嵌入式系统和 PC。
MQTT-C 完全是 线程安全,但也可以在单线程系统上运行良好,使 MQTT-C 非常适合嵌入式系统和微控制器。
MQTT-C 很小 —— 只有两个源文件,代码总计少于 2000 行。
下载、编译 MQTT-C 源码包
下载源码包并解压
MQTT-C 的源码仓库 MQTT-C 位于GitHub上。
unzip MQTT-C-master.zip
编译 MQTT-C 源码包
这里使用 MQTT-C 源码包中提供的 makefile 进行编译。
MQTT-C 使用 cmocka 单元测试框架进行单元测试。 因此,要安装 cmocka 才能构建和运行单元测试。
如果不想安装 cmocka 在 makefile 中将下图第 12 行注释掉就可以。
在解压后的源码目录执行编译
make
如果没有安装 OpenSSL 的开发库,会出现如下编译报错:
gcc -Wextra -Wall -std=gnu99 -Iinclude -Wno-unused-parameter -Wno-unused-variable -Wno-duplicate-decl-specifier `pkg-config --cflags openssl` -D MQTT_USE_BIO examples/bio_publisher.c src/mqtt.c src/mqtt_pal.c -lpthread `pkg-config --libs openssl` -o bin/bio_publisher
Package openssl was not found in the pkg-config search path.
Perhaps you should add the directory containing `openssl.pc'
to the PKG_CONFIG_PATH environment variable
No package 'openssl' found
Package openssl was not found in the pkg-config search path.
Perhaps you should add the directory containing `openssl.pc'
to the PKG_CONFIG_PATH environment variable
No package 'openssl' found
In file included from include/mqtt.h:43,
from examples/bio_publisher.c:11:
include/mqtt_pal.h:100:22: fatal error: openssl/bio.h: No such file or directory
100 | #include <openssl/bio.h>
| ^~~~~~~~~~~~~~~
compilation terminated.
In file included from include/mqtt.h:43,
from src/mqtt.c:25:
include/mqtt_pal.h:100:22: fatal error: openssl/bio.h: No such file or directory
100 | #include <openssl/bio.h>
| ^~~~~~~~~~~~~~~
compilation terminated.
In file included from include/mqtt.h:43,
from src/mqtt_pal.c:25:
include/mqtt_pal.h:100:22: fatal error: openssl/bio.h: No such file or directory
100 | #include <openssl/bio.h>
| ^~~~~~~~~~~~~~~
compilation terminated.
make: *** [makefile:24: bin/bio_publisher] Error 1
安装 OpenSSL 的开发库 libssl-dev 即可
sudo apt install libssl-dev
编译出来的可执行程序在 bin 中:
test@test-virtual-machine:~/work/MQTT-C-master$ ls bin/
bio_publisher openssl_publisher reconnect_subscriber simple_publisher simple_subscriber
测试
使用 simple_subscriber 进行简单测试:
simple_subscriber 是一个简单的程序,每当按下 Enter 键时,它就会发布当前时间。
这里使用的 simple_subscriber 是对源文件 MQTT-C-master/examples/simple_publisher.c 进行调整后编译出来的,调整如下:
将
mqtt_connect(&client, client_id, NULL, NULL, 0, NULL, NULL, connect_flags, 400);
调整为
char *will_message = "bye-bye";
mqtt_connect(&client, client_id, "datetime", will_message, strlen(will_message), "MQTTx1", "admin", connect_flags, 400);
下图给出了 mqtt_connect 函数的原型,及参数定义,从这可以看出此次做了两方面调整:
- 通过用户名和密码来进行相关的认证。
- 设置了遗嘱消息。
遗嘱消息是 MQTT 为那些可能出现意外断线的设备提供的将遗嘱优雅地发送给其他客户端的能力。
设置了遗嘱消息的 MQTT 客户端异常下线时,MQTT 服务器会发布该客户端设置的遗嘱消息。
mqtt_connect 函数通过参数 will_topic 设置遗嘱消息的 Topic 。
调整过的 simple_publisher.c 如下所示:这里主要看 main 函数,其它内容省略
从下面代码中可以看出
- 服务器的端口号默认为:1883
- 发布当前时间的 Topic 默认为:datetime
/**
* @file
* A simple program to that publishes the current time whenever ENTER is pressed.
*/
......
int main(int argc, const char *argv[])
{
const char* addr;
const char* port;
const char* topic;
/* get address (argv[1] if present) */
if (argc > 1) {
addr = argv[1];
} else {
addr = "test.mosquitto.org";
}
/* get port number (argv[2] if present) */
if (argc > 2) {
port = argv[2];
} else {
port = "1883";
}
/* get the topic name to publish */
if (argc > 3) {
topic = argv[3];
} else {
topic = "datetime";
}
/* open the non-blocking TCP socket (connecting to the broker) */
int sockfd = open_nb_socket(addr, port);
if (sockfd == -1) {
perror("Failed to open socket: ");
exit_example(EXIT_FAILURE, sockfd, NULL);
}
/* setup a client */
struct mqtt_client client;
uint8_t sendbuf[2048]; /* sendbuf should be large enough to hold multiple whole mqtt messages */
uint8_t recvbuf[1024]; /* recvbuf should be large enough any whole mqtt message expected to be received */
mqtt_init(&client, sockfd, sendbuf, sizeof(sendbuf), recvbuf, sizeof(recvbuf), publish_callback);
/* Create an anonymous session */
const char* client_id = NULL;
/* Ensure we have a clean session */
uint8_t connect_flags = MQTT_CONNECT_CLEAN_SESSION;
/* Send connection request to the broker. */
char *will_message = "bye-bye";
mqtt_connect(&client, client_id, "datetime", will_message, strlen(will_message), "MQTTx1", "admin", connect_flags, 400);
/* check that we don't have any errors */
if (client.error != MQTT_OK) {
fprintf(stderr, "error: %s\n", mqtt_error_str(client.error));
exit_example(EXIT_FAILURE, sockfd, NULL);
}
/* start a thread to refresh the client (handle egress and ingree client traffic) */
pthread_t client_daemon;
if(pthread_create(&client_daemon, NULL, client_refresher, &client)) {
fprintf(stderr, "Failed to start client daemon.\n");
exit_example(EXIT_FAILURE, sockfd, NULL);
}
/* start publishing the time */
printf("%s is ready to begin publishing the time.\n", argv[0]);
printf("Press ENTER to publish the current time.\n");
printf("Press CTRL-D (or any other key) to exit.\n\n");
while(fgetc(stdin) == '\n') {
/* get the current time */
time_t timer;
time(&timer);
struct tm* tm_info = localtime(&timer);
char timebuf[26];
strftime(timebuf, 26, "%Y-%m-%d %H:%M:%S", tm_info);
/* print a message */
char application_message[256];
snprintf(application_message, sizeof(application_message), "The time is %s", timebuf);
printf("%s published : \"%s\"", argv[0], application_message);
/* publish the time */
mqtt_publish(&client, topic, application_message, strlen(application_message) + 1, MQTT_PUBLISH_QOS_0);
/* check for errors */
if (client.error != MQTT_OK) {
fprintf(stderr, "error: %s\n", mqtt_error_str(client.error));
exit_example(EXIT_FAILURE, sockfd, &client_daemon);
}
}
/* disconnect */
printf("\n%s disconnecting from %s\n", argv[0], addr);
sleep(1);
/* exit */
exit_example(EXIT_SUCCESS, sockfd, &client_daemon);
}
......
执行下面命令:192.168.0.107 是 MQTT Broker (即 emqx )宿主机的 IP地址。
通过 wireshark 抓包,在 connect 阶段的报文如下所示:MQTT 为应用层协议,传输层使用 TCP
从上图可以看出,在 Publish 与 Broker 建立 TCP 连接后,Publish 向 Broker 发送了一个 Connect Command ,报文中 MQTT 协议部分携带的信息就是调用 mqtt_connect 函数时传入的参数。
在 Broker 收到 Connect Command 后, Broker 又向 Publish 回了一个 Connect Ack 。
mqtt_connect 函数中参数 keep_alive(即保活周期)设置为 400s:
打开 mqttx 做 Subscribe,其配置如下,点击连接:
可以看连接成功以后,Subscribe1 订阅的两个 Topic,其中 publish-test 是之前使用Subscribe1遗留的不用管。
可以看到 Subscribe 与 Broker 建立连接时的报文交互:
连接建立成功后,Subscribe1 接着向 Broker 接连发送了两个订阅报文 Subscribe Request (id=5186) [publish-test] 与 Subscribe Request (id=5187) [datetime] 。
Publish 发布当前时间:
报文如下:
Broker 在收到 Publish 发布的 Topic 为 datetime 的消息后,又将消息分发给了订阅 Topic 为 datetime 的 Subscribe1
异常终止掉进程 simple_publisher (即 Publish) :
可以看到断开 Publish 与 Broker TCP 连接的 4 次挥手报文,之后 Broker 就向 Subscribe1 发送了 Topic 为 datetime 的消息,消息的内容为 “bye-bye”,这是 Publish 设置的遗嘱消息。
下图展示了 Subscribe1 收到的消息,只用关注最后两条消息即可,其它消息为前次使用遗留。