两种MQTT协议可视化调试工具
一、MQTT技术背景
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一个基于客户端-服务器的消息发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上 ,MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
MQTT主要用于服务端对客户端进行消息推送 ,MQTT消息推送是基于主题topic
模式的:
- 客户端发布一条消息时,必须指定消息主题。(如,topic=”天气”,payload=”北京今天雾霾好大啊~~呜呜”),其中topic就是主题,payload是发送的具体内容。
- 服务端推送消息,也是基于主题的。当服务器发现有主题(如,topic=“天气”)时,就会给所有订阅该主题的客户端推送payload内容。
- 这里需要个前提,就是有客户端订阅topic=”天气”这个主题;
- 一旦客户端订阅该主题,服务端就会每收到该主题的消息,都会推送给订阅该主题的客户端。如果客户端不需要关注该主题了,也就是说不想接受到这样的推送消息了,只要取消otpic=”天气”的主题订阅即可。
由于物联网的环境是非常特别的,所以MQTT遵循以下设计原则:
(1)精简,不添加可有可无的功能;
(2)发布/订阅(Pub/Sub)模式,方便消息在传感器之间传递;
(3)允许用户动态创建主题,零运维成本;
(4)把传输量降到最低以提高传输效率;
(5)把低带宽、高延迟、不稳定的网络等因素考虑在内;
(6)支持连续的会话控制;
(7)理解客户端计算能力可能很低;
(8)提供服务质量管理;
(9)假设数据不可知,不强求传输数据的类型与格式,保持灵活性。
在通讯过程中,MQTT协议中有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。
MQTT传输的消息分为:主题(Topic)和负载(payload)两部分:
(1)Topic,可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload);
(2)payload,可以理解为消息的内容,是指订阅者具体要使用的内容。
二、MQTT消息收发模型
消息队列 MQ 支持“发布-订阅”模型,消息发布者(生产者)可以将一条消息发送服务端的某个主题(Topic),多个消息接收方(消费者)订阅这个主题以接收该消息。
图 2.1 MQTT消息收发模型
三、两种MQTT可视化调试工具
现在生产中用的最多的消息队列有Activemq,rabbitmq,kafka,rocketmq等。Apache RocketMQ是阿里开源的一款高性能、高吞吐量的分布式消息中间件 。
1.通过docker容器部署RocketMQ控制台
①在虚拟机中安装社区版docker;
②在github上下载RocketMQ项目到虚拟机本地https://github.com/apache/rocketmq-externals
③进入/rocketmq/rocketmq-externals-master/rocketmq-docker/4.3.0
④执行脚本./play-docker.sh
huangxiang6@ubuntu:$ ./play-docker.sh
Sending build context to Docker daemon 12.8kB
Step 1/10 : FROM centos:7
7: Pulling from library/centos
256b176beaff: Pull complete
Digest: sha256:6f6d986d425aeabdc3a02cb61c02abb2e78e57357e92417d6d58332856024faf
Status: Downloaded newer image for centos:7
---> 5182e96772bf
...
Complete!
Loaded plugins: fastestmirror, ovl
Cleaning repos: base extras updates
Cleaning up everything
Maybe you want: rm -rf /var/cache/yum, to also free up space taken by orphaned data from disabled or removed repos
Cleaning up list of fastest mirrors
Removing intermediate container 82d618319d33
---> fdd0f7dd9c15
Step 3/10 : ARG version
---> Running in cc83f4b1f74e
...
Step 10/10 : VOLUME /opt/logs /opt/store
---> Running in d265bb7354cb
Removing intermediate container d265bb7354cb
---> 1319d34041ff
Successfully built 1319d34041ff
Successfully tagged apache/rocketmq-base:4.3.0
Sending build context to Docker daemon 3.072kB
Step 1/4 : FROM apache/rocketmq-base:4.3.0
---> 1319d34041ff
...
Successfully built cb3c05246a87
Successfully tagged apache/rocketmq-broker:4.3.0
1029dd260e6a50eebec0345a787e0cfe72b71638b263cf41147fed104e0cfa6d
⑤运行如下命令自动从docker hub上拉取MQTT控制台镜像,namesrv.addr为自己虚拟主机的ip,同时配置监听端口8088:
#mvn clean package -Dmaven.test.skip=true docker:build
#docker pull styletang/rocketmq-console-ng
#运行docker run 自动拉取控制台镜像,更改namesrv.addr.默认情况下,nameserver监听的是9876端口。
docker run -e "JAVA_OPTS=-Drocketmq.namesrv.addr=10.197.163.241:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" -p 8088:8080 -d --name rmq-dashboard styletang/rocketmq-console-ng
⑥容器运行正常后可以通过浏览器访问RocketMQ控制台,通过订阅服务器发布的主题,可以查看到详细的消息传递流程:
图3.1 通过docker容器构建的MQ控制台
2.MQTT控制台mqtt.fx
mqtt.fx是一款小巧的桌面级应用, 可以快速简单的对mqtt协议进行调试和测试。
MQTT.fx 是目前主流的mqtt客户端,可以快速验证是否可以与IoT Hub 服务交流发布或订阅消息。设备将当前所处的状态作为MQTT主题发送给IoT Hub,每个MQTT主题topic具有不同等级的名称,如“建筑/楼层/温度。” MQTT代理服务器将接收到的主题topic发送给给所有订阅的客户端。
mqtt.fx调试工具下载链接:http://www.jensd.de/apps/mqttfx/1.7.1/
表3.2 mqtt.fx属性
属性 | MQTTBox |
---|---|
类型 | 图形用户界面,基于JavaFX |
执照 | Apache 2.0许可证 |
操作系统 | Mac OSX,Windows和Linux |
网站 | https://mqttfx.jensd.de/ |
MQTT协议默认端口号为1883,若使用SSL,则为8883. 首先需要配置mqtt.fx客户端,设备端在保活时间间隔内,至少需要发送一次报文,包括ping请求。如果物联网平台在保活时间内无法收到任何报文,物联网平台会断开连接,设备端需要进行重连。连接保活时间的取值范围为30至1200秒。建议取值300秒以上。
图3.2 配置mqtt.fx启动选项
四、测试案例
以mqtt.fx工具为例说明。
1.环境
服务器端使用阿里云物联网中间件RocketMQ作为生产者发布主题消息;
客户端为mqtt_demo运行在本地虚拟机ubuntu,作为消费者接收服务器发布的mq消息;
消息队列 RocketMQ 快速接入流程图:
图4.1 RocketMQ接入流程
2.步骤
①登录阿里云主页,依次打开产品 > 企业应用 > 消息队列 MQ ,单击消息队列 RocketMQ进入消息队列 RocketMQ 的产品主页。
②在消息队列 RocketMQ 的产品主页上,单击立即开通进入消息队列 RocketMQ 服务开通页面,根据提示完成开通服务。
③创建资源:一个新的应用接入消息队列 RocketMQ 需要先创建相关的消息队列 RocketMQ 资源,包括:
- **实例:**用于消息队列 RocketMQ 服务的虚拟机资源,会存储消息主题(Topic)和客户端 ID(Group ID)信息。
图4.2.1 阿里云服务器创建MQ实例
- **消息主题(Topic):**在消息队列 RocketMQ 的消息系统中,消息生产者将消息发送到某个指定的 Topic ,而消息消费者则通过订阅该指定的 Topic 来获取和消费消息。
图4.2.2 阿里云服务器创建Topic
- **Group ID:**用于消息消费者(或生产者)的标识 ,在MQ控制台 GroupID管理界面,查看设备信息,GID_hik@@@ClientID_hik在线,并且订阅了Topic 为hik的消息。此处的ClientID是在mqtt_demo代码中手动配置的。
图4.2.3 阿里云服务器添加消费者标识
- **阿里云 AccessKey:**用于收发消息时进行账户鉴权
④将 Topic 和 Group ID 都创建在“公网”地域下的实例中。 生产端和消费端可以部署在本地或者部署在任意地域的 ECS 上,前提是本地服务器或者相应的 ECS 需要能够访问公网,实例创建好后可在阿里云控制台看到生产者信息:
图4.2.4 生产者发布主题消息
⑤实例创建完成之后,在本地虚拟机运行mqtt_demo(代码见附录),接收服务器端消息队列实例实时的主题和内容,客户端运行正常之后可在阿里云控制台看到客户端配置信息:
图4.2.4 虚拟机作为消费者
⑥下行通信测试
从物联网平台发送消息,在MQTT.fx上接收消息,测试MQTT.fx与物联网平台连接是否成功 。
- 在MQTT.fx上,单击Subscribe。
- 输入一个设备下的Topic,然后单击Subscribe,订阅这个Topic。例如hik,可接收到订阅主题的内容Hello Hikvision!订阅成功后,该Topic将显示在列表中。
⑦在Log选项中可以看到客户端连接到服务器的过程,订阅主题和收取主题内容的过程日志、操作日志和错误提示日志:
3.总结
mqtt.fx工具可以接入物联网平台并监控MQTT客户端与服务器之间的通信过程,此工具需要依赖在物联网平台控制台创建的产品和设备的三元组信息:ProductKey、DeviceName和DeviceSerect。连接建立成功之后,可以通过mqtt.fx模拟客户端订阅服务器MQTT主题,来校验消息内容和查看连接过程。
五、附录
mqtt客户端demo代码:
//MQTTDemo.c
#include "MQTTAsync.h"
#include <signal.h>
#include <memory.h>
#include <stdlib.h>
#include <unistd.h>
#include <openssl/hmac.h>
#include <openssl/bio.h>
volatile int connected = 0;
char *topic;
char *userName;
char *passWord;
int messageDeliveryComplete(void *context, MQTTAsync_token token) {
/* not expecting any messages */
printf("send message %d success\n", token);
return 1;
}
int messageArrived(void *context, char *topicName, int topicLen, MQTTAsync_message *m) {
/* not expecting any messages */
printf("recv message from %s ,body is %s\n", topicName, (char *) m->payload);
MQTTAsync_freeMessage(&m);
MQTTAsync_free(topicName);
return 1;
}
void onConnectFailure(void *context, MQTTAsync_failureData *response) {
connected = 0;
printf("connect failed, rc %d\n", response ? response->code : -1);
MQTTAsync client = (MQTTAsync) context;
}
void onSubcribe(void *context, MQTTAsync_successData *response) {
printf("subscribe success \n");
}
void onConnect(void *context, MQTTAsync_successData *response) {
connected = 1;
printf("connect success \n");
MQTTAsync client = (MQTTAsync) context;
//do sub when connect success
MQTTAsync_responseOptions sub_opts = MQTTAsync_responseOptions_initializer;
sub_opts.onSuccess = onSubcribe;
int rc = 0;
if ((rc = MQTTAsync_subscribe(client, topic, 1, &sub_opts)) != MQTTASYNC_SUCCESS) {
printf("Failed to subscribe, return code %d\n", rc);
}
}
void onDisconnect(void *context, MQTTAsync_successData *response) {
connected = 0;
printf("connect lost \n");
}
void onPublishFailure(void *context, MQTTAsync_failureData *response) {
printf("Publish failed, rc %d\n", response ? -1 : response->code);
}
int success = 0;
void onPublish(void *context, MQTTAsync_successData *response) {
printf("send success %d\n", ++success);
}
void connectionLost(void *context, char *cause) {
connected = 0;
MQTTAsync client = (MQTTAsync) context;
MQTTAsync_connectOptions conn_opts = MQTTAsync_connectOptions_initializer;
int rc = 0;
printf("Connecting\n");
conn_opts.MQTTVersion = MQTTVERSION_3_1_1;
conn_opts.keepAliveInterval = 60;
conn_opts.cleansession = 1;
conn_opts.username = userName;
conn_opts.password = passWord;
conn_opts.onSuccess = onConnect;
conn_opts.onFailure = onConnectFailure;
conn_opts.context = client;
//如果使用加密ssl的方式,此处需要初始化,否则设置为NULL
conn_opts.ssl = NULL;
//MQTTAsync_SSLOptions ssl =MQTTAsync_SSLOptions_initializer;
//conn_opts.ssl = &ssl;
if ((rc = MQTTAsync_connect(client, &conn_opts)) != MQTTASYNC_SUCCESS) {
printf("Failed to start connect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
}
int main(int argc, char **argv) {
MQTTAsync_disconnectOptions disc_opts = MQTTAsync_disconnectOptions_initializer;
MQTTAsync client;
//MQTT 使用的 Topic,其中第一级父 Topic 需要在 MQ 控制台先创建
topic = "TOPIC_DEVICE_MQTT_MSG_TEST";
//MQTT 的接入域名,在 MQ 控制台购买付费实例后即分配该域名,如果是使用加密ssl的方式,域名格式是ssl://XXX.mqtt.aliyuncs.com 注意!!!!在C接入中host不能有前缀tcp://和端口号:1883,否则连接失败!!
char *host = "post-cn-4590tijb90x.mqtt.aliyuncs.com";
//MQTT 客户端分组 Id,需要先在 MQ 控制台创建
char *groupId = "GID_H90";
//MQTT 客户端设备 Id,用户自行生成,需要保证对于所有 TCP 连接全局唯一
char *deviceId = "ClientID_lascala";
//帐号 AK
char *accessKey = "LTAIY7jnr9tBPwaR";
//帐号 SK
char *secretKey = "a4m7JHgugWlHe5csjF3PcWaXwlheia";
//服务端口,使用 MQTT 协议时设置1883,其他协议参考文档选择合适端口,如果是加密ssl的话端口是8883
int port = 1883;
//QoS,消息传输级别,参考文档选择合适的值
int qos = 1;
//cleanSession,是否设置持久会话,如果需要离线消息则 cleanSession 必须是 false,QoS 必须是1
int cleanSession = 0;
int rc = 0;
char tempData[100];
int len = 0;
HMAC(EVP_sha1(), secretKey, strlen(secretKey), groupId, strlen(groupId), tempData, &len);
char resultData[100];
int passWordLen = EVP_EncodeBlock((unsigned char *) resultData, tempData, len);
resultData[passWordLen] = '\0';
printf("passWord is %s", resultData);
userName = accessKey;
passWord = resultData;
//1.create client
MQTTAsync_createOptions create_opts = MQTTAsync_createOptions_initializer;
create_opts.sendWhileDisconnected = 0;
create_opts.maxBufferedMessages = 10;
char url[100];
sprintf(url, "%s:%d", host, port);
char clientIdUrl[64];
sprintf(clientIdUrl, "%s@@@%s", groupId, deviceId);
rc = MQTTAsync_createWithOptions(&client, url, clientIdUrl, MQTTCLIENT_PERSISTENCE_NONE, NULL, &create_opts);
rc = MQTTAsync_setCallbacks(client, client, connectionLost, messageArrived, NULL);
//2.connect to server
MQTTAsync_connectOptions conn_opts = MQTTAsync_connectOptions_initializer;
conn_opts.MQTTVersion = MQTTVERSION_3_1_1;
conn_opts.keepAliveInterval = 60;
conn_opts.cleansession = cleanSession;
conn_opts.username = userName;
conn_opts.password = passWord;
conn_opts.onSuccess = onConnect;
conn_opts.onFailure = onConnectFailure;
conn_opts.context = client;
//如果使用加密ssl的方式,此处需要初始化,否则设置为NULL
conn_opts.ssl = NULL;
//MQTTAsync_SSLOptions ssl =MQTTAsync_SSLOptions_initializer;
//conn_opts.ssl = &ssl;
conn_opts.automaticReconnect = 1;
conn_opts.connectTimeout = 3;
if ((rc = MQTTAsync_connect(client, &conn_opts)) != MQTTASYNC_SUCCESS) {
printf("Failed to start connect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
//3.publish msg
MQTTAsync_responseOptions pub_opts = MQTTAsync_responseOptions_initializer;
pub_opts.onSuccess = onPublish;
pub_opts.onFailure = onPublishFailure;
for (int i = 0; i < 1000; i++) {
do {
char data[100];
//发布主题消息内容
sprintf(data, "Hello Hikvision!");
rc = MQTTAsync_send(client, topic, strlen(data), data, qos, 0, &pub_opts);
sleep(1);
} while (rc != MQTTASYNC_SUCCESS);
}
sleep(1000);
disc_opts.onSuccess = onDisconnect;
if ((rc = MQTTAsync_disconnect(client, &disc_opts)) != MQTTASYNC_SUCCESS) {
printf("Failed to start disconnect, return code %d\n", rc);
exit(EXIT_FAILURE);
}
while (connected)
sleep(1);
MQTTAsync_destroy(&client);
return EXIT_SUCCESS;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.7)
project(mqttdemo)
INCLUDE_DIRECTORIES(
.
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}
)
SET(CMAKE_BUILD_TYPE "Debug")
SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g2 -ggdb")
SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")
set(CMAKE_C_STANDARD 99)
add_executable(mqttDemo mqttDemo.c)
TARGET_LINK_LIBRARIES(mqttDemo crypto)
TARGET_LINK_LIBRARIES(mqttDemo paho-mqtt3as)
异步MQTT头文件MQTTAsync.h
头文件地址:https://github.com/lt-holman/paho.mqtt.c/blob/master/src/MQTTAsync.h