kawaii-mqtt 移植总结:
由于业务需要使用MQTT来传输数据,而对于单片机实现来说,一般就两种方案,第一种是通过网络模块的自带的MQTT通过AT命令或者MQTT透传方式,第二种是模块不具备MQTT协议通过TCP的方式实现MQTT协议。
第一种方式基本基本按照模块AT命令配置好后就可以收发数据,比较容易实现。
第二种方式需要基于TCP的方式移植一个MQTT协议栈。
通过查阅资料MQTT相关的开源软件比较多:
kawaii-mqtt-master // 收录到了RTT软件包中,对MQTT支持比较全面。网上介绍比较多。
MQTT-C-master // 代码比较少,两三个文件。
umqtt-master //RT-Thread 官方维护
RyanMqtt-main //和kawaii-mqtt-master差不多都是对pahoMqtt的封装。维护的比较频繁。
wolfMQTT-master //STM32CubeMX 自带,但网上资料比较少。但是由于STM32CubeMX 自带,移植非常简单,勾选就可以了。
我选用的是kawaii-mqtt-master,主要是网上资料稍微多一点,作者也录了相关移植视频,看了感觉用起来问题不大。但是似乎并不是一帆风顺,经过几天的折腾,终于顺利稳定的跑起来了。记录一下自己踩过的坑。
第一问题
由于在移植过程中没有考虑到int platform_net_socket_recv_timeout(int fd, unsigned char *buf, int len, int timeout)这个函数中,timeout等于0的情况,等于0的时候,只返回一个字节,不管长度len 等于多少,就直接超时退出了。导致订阅总是失败。
int platform_net_socket_recv_timeout(int fd, unsigned char *buf, int len, int timeout)
第二个问题
mqtt_publish这个函数中, if (CLIENT_STATE_CONNECTED != mqtt_get_client_state©) 这里判断失败后,直接goto exit 去platform_mutex_unlock(&c->mqtt_write_lock);,而此时并没有上锁,我使用的freeRTOS ,如果没有上锁的话,开锁直接会断言失败。( configASSERT( pxTCB == pxCurrentTCB )😉 在开锁中判断的这个锁的持有者是否等于当前线程。如果没有上锁,那么这锁的持有者不等于当前线程。解决这个问题只需要将platform_mutex_lock(&c->mqtt_write_lock); 放到if (CLIENT_STATE_CONNECTED != mqtt_get_client_state©) 之前,示例如下:
int mqtt_publish(mqtt_client_t* c, const char* topic_filter, mqtt_message_t* msg)
{
int len = 0;
int rc = KAWAII_MQTT_FAILED_ERROR;
platform_timer_t timer;
MQTTString topic = MQTTString_initializer;
topic.cstring = (char *)topic_filter;
platform_mutex_lock(&c->mqtt_write_lock);// 将锁的位置修改到此处。
if (CLIENT_STATE_CONNECTED != mqtt_get_client_state(c)) {
rc = KAWAII_MQTT_NOT_CONNECT_ERROR;
goto exit;
}
if ((NULL != msg->payload) && (0 == msg->payloadlen))
msg->payloadlen = strlen((char*)msg->payload);
//platform_mutex_lock(&c->mqtt_write_lock); 删除
...
exit:
msg->payloadlen = 0; // clear
platform_mutex_unlock(&c->mqtt_write_lock);
...
RETURN_ERROR(rc);
}
第三个问题
这个问题如果严格的MQTT协议或者网络稳定情况一般不会出现,我这里两种方式会导致这个函数有问题,一种是我没有关闭网路模块的心跳包,每次隔一段时间会上传一段数据(和MQTT协议无关的数据),另一种是MQTT频繁的收发数据,我突然断开模块,一帧MQTT数据可能收发不完整。两种问题都会导致msg_handler = mqtt_get_msg_handler(c, topic_name);返回为空,即msg_handler=NULL。而下面memset(message->payload, 0, message->payloadlen);这里直接导致软件崩溃。解决只需要增加空判断。代码修改如下:
static int mqtt_deliver_message(mqtt_client_t* c, MQTTString* topic_name, mqtt_message_t* message)
{
int rc = KAWAII_MQTT_FAILED_ERROR;
message_handlers_t *msg_handler;
/* get mqtt message handler */
msg_handler = mqtt_get_msg_handler(c, topic_name);
logDebug(" %X %d %s ", msg_handler, topic_name->lenstring.len, topic_name->lenstring.data);
if (NULL != msg_handler) {
message_data_t md;
mqtt_new_message_data(&md, topic_name, message); /* make a message data */
msg_handler->handler(c, &md); /* deliver the message */
rc = KAWAII_MQTT_SUCCESS_ERROR;
} else if (NULL != c->mqtt_interceptor_handler) {
message_data_t md;
mqtt_new_message_data(&md, topic_name, message); /* make a message data */
c->mqtt_interceptor_handler(c, &md);
rc = KAWAII_MQTT_SUCCESS_ERROR;
}
if (NULL == msg_handler || NULL == c->mqtt_interceptor_handler) { RETURN_ERROR(rc); } //增加空判断。
/*payload may not be string, so use meessage->payloadlen memeset zhaoshimin 20200629*/
if (message->payload)memset(message->payload, 0, message->payloadlen);//增加空判断。
if (topic_name->lenstring.data)memset(topic_name->lenstring.data, 0, topic_name->lenstring.len);//增加空判断。
RETURN_ERROR(rc);
}
第四个问题
这个问题的现象是我订阅了2个主题,但是每次第一次定义的主题可以正确的调用回调函数。多次调试后,问题如下:
主题订阅函数调用关系:
mqtt_subscribe 订阅主题函数
mqtt_ack_list_record 记录一下应答事件
mqtt_list_add_tail(&ack_handler->list, &c->mqtt_ack_handler_list); 添加到链表 通过链表管理应答事件
其大致流程是调用订阅函数把订阅的主题放到链表中,等待服务器应答时,在找出对应的应答事件移除链表。放在链表的好处是超时再次重发。
主题订阅应答处理流程:
mqtt_yield 线程处理函数 调用mqtt_packet_handle
mqtt_packet_handle 数据包处理函数
rc = mqtt_suback_packet_handle(c, timer); 第一次处理服务器订阅应答正常,且rc等于0表示处理成功。
if (rc == KAWAII_MQTT_SUCCESS_ERROR) 后续有这个判断,有将rc赋值为报类型,
rc = packet_type;
RETURN_ERROR(rc); 最终返回 订阅主题应答 包类型 为9
mqtt_yield 线程处理函数 调用mqtt_packet_handle 后有一下判断
if (rc >= 0) {
/* scan ack list, destroy ack handler that have timed out or resend them */
mqtt_ack_list_scan(c, 1); }
此时rc等于9。需要执行 mqtt_ack_list_scan(c, 1);
mqtt_ack_list_scan 遍历整个应答的链表,判断是否有超时的节点和根据包类型判断是否需要重发。
最后执行了这两个函数
mqtt_ack_handler_destroy(ack_handler);// 将应答链表移除。此时第二个主题的应答事件被移除了,当再次收到服务器应答时候,应答链表中不存在第二次订阅主题的事件,则反馈应答失败。
mqtt_subtract_ack_handler_num(c);
解决办法:
if ((ack_handler->type == PUBACK) || (ack_handler->type == PUBREC) || (ack_handler->type == PUBREL) || (ack_handler->type == PUBCOMP) || (ack_handler->type == SUBACK))
在mqtt_ack_list_scan函数中增加了:ack_handler->type == SUBACK 判断,逻辑是如果超时了则重发订阅的主题数据包。
导致这个问题情况应答是超时时间和mqtt_packet_handle处理时间间隔的问题,
c->mqtt_cmd_timeout 和 订阅主题时 ack_handler->timer 这两个时间设置的问题,通过源码发现 这个时间设置是一样的,而订阅主题函数几乎是同时调用,第一个主题的ack_handler->timer和第二个主题的ack_handler->timer很有可能相等,那么处理完成了第一个主题确认后,执行mqtt_ack_list_scan,发现第二个主体已经超时了,超时了没做任何处理 直接移除应答事件链表。
当然解决这个问题,还有其他方式,例如重新调整c->mqtt_cmd_timeout 和 订阅主题时 ack_handler->timer 超时时间。
目前发现的这些问题,修改后已经正常跑了一天了。以上的问题理解有不合理的地方欢迎大家指出、讨论。
源码的下载链接:
kawaii-mqtt: 基于socket API之上的跨平台MQTT客户端 (gitee.com)
其他参考链接:
这个问题也可以看看,如果遇到了可以快速修改:记一次解决MQTT软件包内存泄露的心路历程_android mqtt 连接泄漏-CSDN博客