分析QOS机制,并对RT-Thread的paho mqtt软件包 QOS完善支持

引言

由于开发需求要在RT-Thread引用最新的paho mqtt库,然而RTOS端的维护并不像Linux端一样完善,尚且有很多地方需要开发完善,因此参考各方完善的mqtt库进行完善RT-Thread pahomqtt的QOS功能,借此也整理下大体的机制流程,提供大家交流分享。

在初步接触MQTT时,相信对于QOS机制的理解是非常简单的,但要明白其逻辑原理如何实现则需要更深层的剖析。本文主要介绍MQTT协议规范的数据交互过程,其中会详细介绍QOS2等级消息的实现以及QOS2如何保证消息只会到达一次,提供QOS2落实到代码的方法。

有关MQTT协议可以参考文档:《MQTT协议中文版》MQTT协议中文版

本文更多的是对协议设计的合理性进行解读,更加具体的协议流程可参考该文档。

QOS各等级特性简介

QOS0

QOS0等级的消息对于发送者而言只会发送一次,不关心接收者是否收到,对于接收者而言至多收到一次数据。

对于双方操作流程:

  1. 发送者将消息发送一次后直接释放掉原始消息,放弃消息所有权
  2. 接收者一旦接收到消息便意味着直接接管了消息的所有权

QOS0的数据丢失完全依赖于网络环境以及应用处理,但QOS0并不代表绝对的数据不安全,MQTT透传是在TCP/IP基础载体上,增加了MQTT协议,数据是跑在TCP/IP载体上的,实际的数据是通过TCP传输的,因此TCP保证了QOS0最基本的传输稳定。

QOS1

QOS1等级的消息需要发送者发送完毕后等待接收确认的信号,对于接收者而言至少收到一次数据。

对于双方操作流程:

  1. 发送消息需要带上未被使用的msg id。
  2. 发送者发送消息后需要将消息看作未确认的,并进行保存,在收到PUBACK报文后将所有权转移给接收者。
  3. 发送者未收到回复需要重发消息,重发消息的报文必须带有重发标识。
  4. 接收者在接收到消息时需要先回复PUBACK报文后再向前传递。
  5. 接收者在发送完回复消息报文后,后续接收任何msg id都应该当作一条新消息进行接收(多次收到数据便是在此发生)。

对于双方而言,消息丢失可能发生在任意一次交互,这样都会导致消息进行重传,而重传的消息应在报文中增加重发标志(DUP)提供接收者自己甄别。

该机制存在一个明显的缺陷:QOS1交互通过msg id来识别是否为同一条消息,但双方都无法得知对方的msg id是否删除,这便是导致重复接收的原因,QOS2便针对此处进行了优化。

QOS2

QOS2等级的消息在逻辑层面可以做到消息的仅有一次到达,消息的重复和丢失都是不被允许的,通过两步确认实现消息的传递。QOS2的实现需要发送与接收双方都能正确处理消息,实现的复杂度增加。

对于双方操作流程:

  1. 发送消息需要带上未被使用的msg id。
  2. 第一步,发送者发送消息后需要将消息看作未确认的,并进行保存,在收到PUBREC报文后将所有权转移给接收者。
  3. 发送者接收到回复之后便可以将消息销毁,随后进入第二步来确认接收者的数据是否上传给应用。
  4. 发送者进入第二步发送PUBREL后记录msg id,后续重发PUBREL包,不再重发PUBLISH。
  5. 接收者接收到消息后保存msg id,在回复PUBCOMP之前,是不会接收相同msg id的PUBLISH的。
  6. PUBCOMP报文预示着接收者已经将数据投递给上层应用并且处理完毕,在此之前msg id将会一直被接收者占用。
  7. 接收者收到消息后需要将消息本地化,待到收到PUBREL之后才会向上层应用分发。

QOS2等级的消息实现仅有一次到达,实质上是指接收者接收到数据只会向上传递一次,重复收到的数据包可以通过协议流程来过滤掉。

QOS2的操作流程复杂,其丢包后是如何处理来解决重发导致的多包交互呢,过程分析如下:

  1. PUBLISH丢失:该阶段逻辑同QOS1,接收者还没有接触到协议流程,发送者会自动进行重发
  2. PUBREC丢失或未发送:此时接收者已经接收到消息了,发送者由于未收到回复会继续重发PUBLISH,直到收到回复
  3. PUBLISH多次接收:与QOS1不同的是,QOS2只有首次收到PUBLISH报文才会进行处理,其余只会进行协议回复,不进行额外操作,这样便避免了重复操作
  4. PUBREL丢失或未发送:此时发送者因为没有收到回复进行重发
  5. PUBREL多次接收:接收者多次收到相同ID的PUBREL消息,只会在首次接收到PUBREL时将消息传递给上层应用,并且将消息ID删除释放,再执行回复,后续重复收到PIBREL只会进行协议交互,在此避免重复操作
  6. PUBCOMP丢失或未发送:此时接收者已经将消息上传给上层应用,发送者继续重发PUBREL报文,直到收到回复

QOS2在流程上便保证了消息的使用只会发送一次,但同时延长了消息传输的时间,降低了效率。

QOS报文简介(了解)

mqtt协议的报文格式组成由固定报头、可变报头、有效载荷三部分组成,以下仅根据在消息投递过程中不同QOS等级所使用到的报文进行展开说明。

PUBLISH

PUBLISH控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息,长度不固定。

固定报头

DUP(重发标志):如果DUP标志被设置为0,表示这是客户端或服务端第一次请求发送这个PUBLISH报文。如果DUP标志被设置为1,表示这可能是一个早前报文请求的重发。

QOS(服务质量等级):第1字节1~2位标志QOS消息等级(0、1、2),用于接收者进行回复,不可全为1。

RETAIN(保留消息):该位置1,则服务器需要将该条消息的主题、消息内容、QOS等级进行持久化保留,发送未来给每一个新的订阅者。

剩余长度:为可变报头与有效载荷的总和,可变报头包含两字节主题名长度、主题名、两字节报文标识符,据此可推算出有效载荷的数据长度。

可变报头

可变报头包含主题名长度、主题名以及报文标识符(2 byte)。

有效载荷

有效载荷内容是要分发的消息内容,消息长度通过计算固定报头中的剩余长度减去可变报头的长度,长度可以为0。

PUBACK(QOS1)

PUBACK控制报文是接收者接收到QOS1等级的PUBLISH报文消息后需要回复给发送者的报文,长度固定4字节。

固定报头

可变报头

PUBACK回复信息中的报文标识符应与PUBLISH中的相同,通知对方该报文标识符的数据交互完毕。

有效载荷

PUBACK报文没有有效载荷部分。

PUBREC(QOS2)

PUBACK控制报文是接收者接收到QOS2等级的PUBLISH报文消息后需要回复给发送者的报文,长度固定4字节。

固定报头

可变报头

PUBREC回复信息中的报文标识符应与PUBLISH中的相同,通知对方该报文标识符携带的数据接收成功。

有效载荷

PUBACK报文没有有效载荷部分。

PUBREL(QOS2)

PUBREL控制报文可以使发送者确保接收者将消息处理完毕,在接收到PUBREC报文后,便预示着消息已经传递给对方,发送PUBREL便是要等待对方将报文标识符释放,长度固定4字节。

固定报头

可变报头

PUBREL回复信息中的报文标识符应与PUBREC中的相同,开始确认对方是否将该报文标识符携带的数据使用完毕。

有效载荷

PUBACK报文没有有效载荷部分。

PUBCOMP(QOS2)

PUBCOMP控制报文用于接收者通知QOS2消息发送者,消息已经接收处理完毕,报文标识符可以重用,长度固定4字节。

固定报头

可变报头

PUBCOMP回复信息中的报文标识符应与PUBREL中的相同,通知对方该报文标识符携带的数据使用完毕。

有效载荷

PUBACK报文没有有效载荷部分。

QOS2实现方案(全文核心)

经过以上的处理流程以及协议解读,可以看出QOS2等级消息的传递是复杂且占用资源的。下图为《MQTT协议中文版》对QOS2操作流程的解读。

方法A的实现逻辑符合标准认知的MQTT QOS2实现,但是其消息处理的延迟以及资源的占用相较于方法B高出太多。

方法A与方法B同样都可以事项QOS2等级消息的传输安全性,因此本段主要以介绍两种方法的基本流程,以及两方法的功能对比,在文后还会列出笔者在具体项目中通过方法B实现的队列代码以及讲解,为各位后续开发提供参考思路。

逻辑实现流程

具体代码操作流程以及注意点可以解析如下

客户端作为发送者:

  1. 实现两个用于等待的链表,一个用于保存PUBLISH中的消息链表,一个用于保存PUBREL中的报文标识符的ID链表,需要保存时间信息用于检测超时。
  2. 收到PUBREC后便将对应报文标识符的消息链表转化为ID链表保存。
  3. 收到PUBCOMP后将对应报文标识符的消息在对应的ID链表中删除。
  4. 定时检查链表超时并进行重发,无固定时间间隔标准要求。

客户端发送QOS2消息的实现方法并不统一,在kawaii-mqtt中作者设计了等待ack链表,将所有需要等待的消息统一管理存储,实现方式不同但本质目的都是对报文消息的管理。

在下一节示例中会进行kawaii-mqtt的QOS发送代码实现介绍。

客户端以方法A作为接收者:

  1. 实现msg队列用于储存消息内容和报文标识符,队列元素应增加传输标志用于区分是否可以向应用层发送
  2. 接收到PUBLISH后检查报文标识符是否在队列中,不在则将消息入队列。
  3. 接收到PUBREL后,证明该消息可以向应用传递,将传输标志置位。
  4. 注意消息的先后顺序,队列头若传输标志未置位,后续就算全部置位也不可以越过队列头提前传递给应用。
  5. 队列消息传递给应用后便剔除队列,回复PUBCOMP。

客户端以方法B作为接收者:

  1. 实现用于保存报文标识符的链表。
  2. 接收到PUBLISH后检查报文标识符是否在链表中,不在则将报文标识符插入链表,同时将消息向应用传递,在则跳过直接回复PUBREC。
  3. 接收到PUBREL后,将报文标识符在链表中删除,回复PUBCOMP。

对比两种方法实现接收:

  1. 方法A传输时机符合标准MQTT协议要求,但是时机相比方法B较晚,且会导致后续消息阻塞,消息延迟加大。
  2. 方法A需要单独增加缓存消息的空间,占用资源较大,方法B则只需要保存报文标识符。
  3. 方法A的操作逻辑较复杂,操作流程较方法B多且复杂,实现较为困难。
  4. 方法A的队列操作不频繁,操作快速,方法B则需要持续操作链表,相较于方法A占用运行时间。

两种操作方式各有特点,最终都可以实现QOS2的通信流程,因此选择一种方式进行实现即可。

QOS2实现示例

发送QOS2(以kawaii-mqtt为例)

链表操作

链表的插入和弹出操作没有特殊之处,核心在于结构体类型中包含了各种数据包所需要操作的数据内容。

// 链表保存,消息类型以及要重发的消息内容
typedef struct ack_handlers {
    mqtt_list_t         list;
    platform_timer_t    timer;			// 超时时间
    uint32_t            type;			// 要等待的包类型,根据此来判断是否需要重发
    uint16_t            packet_id;		// 报文标识符
    message_handlers_t  *handler;		// 订阅or取消订阅,主题信息结构体链表
    uint16_t            payload_len;	// 数据消息长度
    uint8_t             *payload;		// 数据消息地址
} ack_handlers_t;

/**
 * @brief 将等待的包类型以及相应数据放入ack链表
 * 
 * @param c             mqtt结构体句柄
 * @param type          等待的数据包类型
 * @param packet_id     报文标识符
 * @param payload_len   数据长度
 * @param handler       订阅主题信息结构体(在等待SUBACK、UNSUBACK时使用,其余填充NULL)
 * @return int 
 */
static int mqtt_ack_list_record(mqtt_client_t* c, int type, uint16_t packet_id, uint16_t payload_len, message_handlers_t* handler)
{
    int rc = MQTT_SUCCESS_ERROR;
    ack_handlers_t *ack_handler = NULL;
    
    /* Determine if the node already exists */
    if (mqtt_ack_list_node_is_exist(c, type, packet_id))
        RETURN_ERROR(MQTT_ACK_NODE_IS_EXIST_ERROR);

    /* create a ack handler node */
    ack_handler = mqtt_ack_handler_create(c, type, packet_id, payload_len, handler);
    if (NULL == ack_handler)
        RETURN_ERROR(MQTT_MEM_NOT_ENOUGH_ERROR);

    mqtt_add_ack_handler_num(c);

    mqtt_list_add_tail(&ack_handler->list, &c->mqtt_ack_handler_list);

    RETURN_ERROR(rc);
}

/**
 * @brief 将等待的包类型冲ack链表中删除
 * 
 * @param c             mqtt结构体句柄
 * @param type          等待的数据包类型
 * @param packet_id     报文标识符
 * @param handler       订阅主题信息结构体(在等待SUBACK、UNSUBACK时使用,其余填充NULL)
 * @return int          
 */
static int mqtt_ack_list_unrecord(mqtt_client_t* c, int type, uint16_t packet_id, message_handlers_t **handler)
{
    mqtt_list_t *curr, *next;
    ack_handlers_t *ack_handler;

    if (mqtt_list_is_empty(&c->mqtt_ack_handler_list))
        RETURN_ERROR(MQTT_SUCCESS_ERROR);

    LIST_FOR_EACH_SAFE(curr, next, &c->mqtt_ack_handler_list) {
        ack_handler = LIST_ENTRY(curr, ack_handlers_t, list);

        if ((packet_id != ack_handler->packet_id) || (type != ack_handler->type))
            continue;

        if (handler)
            *handler = ack_handler->handler;
        
        /* destroy a ack handler node */
        mqtt_ack_handler_destroy(ack_handler);
        mqtt_subtract_ack_handler_num(c);
    }
    RETURN_ERROR(MQTT_SUCCESS_ERROR);
}
发送PUBLISH后操作

相关操作请关注下方代码块43~55行:

根据推送消息的QOS等级,将要等待的数据类型(PUBREC)插入到ack链表中。

int mqtt_publish(mqtt_client_t* c, const char* topic_filter, mqtt_message_t* msg)
{
    int len = 0;
    int rc = MQTT_FAILED_ERROR;
    platform_timer_t timer;
    MQTTString topic = MQTTString_initializer;
    topic.cstring = (char *)topic_filter;

    if (CLIENT_STATE_CONNECTED != mqtt_get_client_state(c)) {
        msg->payloadlen = 0;        // clear
        rc = MQTT_NOT_CONNECT_ERROR;
        RETURN_ERROR(rc);              
        // goto exit;  /* 100ask */
    }

    if ((NULL != msg->payload) && (0 == msg->payloadlen))
        msg->payloadlen = strlen((char*)msg->payload);

    if (msg->payloadlen > c->mqtt_write_buf_size) {
        MQTT_LOG_E("publish payload len is is greater than client write buffer...");
        RETURN_ERROR(MQTT_BUFFER_TOO_SHORT_ERROR);
    }

    platform_mutex_lock(&c->mqtt_write_lock);

    if (QOS0 != msg->qos) {
        if (mqtt_ack_handler_is_maximum(c)) {
            rc = MQTT_ACK_HANDLER_NUM_TOO_MUCH_ERROR; /* the recorded ack handler has reached the maximum */
            goto exit;
        }
        msg->id = mqtt_get_next_packet_id(c);
    }
    
    /* serialize publish packet and send it */
    len = MQTTSerialize_publish(c->mqtt_write_buf, c->mqtt_write_buf_size, 0, msg->qos, msg->retained, msg->id,
              topic, (uint8_t*)msg->payload, msg->payloadlen);
    if (len <= 0)
        goto exit;
    
    if ((rc = mqtt_send_packet(c, len, &timer)) != MQTT_SUCCESS_ERROR)
        goto exit;
    
    if (QOS0 != msg->qos) {
        mqtt_set_publish_dup(c, 1);  /* may resend this data, set the dup flag in advance */
        
        if (QOS1 == msg->qos) {
            /* expect to receive PUBACK, otherwise data will be resent */
            rc = mqtt_ack_list_record(c, PUBACK, msg->id, len, NULL);  
            
        } else if (QOS2 == msg->qos) {
            /* expect to receive PUBREC, otherwise data will be resent */
            rc = mqtt_ack_list_record(c, PUBREC, msg->id, len, NULL);   
        }
    }
    
exit:
    msg->payloadlen = 0;        // clear

    platform_mutex_unlock(&c->mqtt_write_lock);

    if ((MQTT_ACK_HANDLER_NUM_TOO_MUCH_ERROR == rc) || (MQTT_MEM_NOT_ENOUGH_ERROR == rc)) {
        MQTT_LOG_W("%s:%d %s()... there is not enough memory space to record...", __FILE__, __LINE__, __FUNCTION__);

        /*must realse the socket file descriptor zhaoshimin 20200629*/
        network_release(c->mqtt_network);
        
        /* record too much retransmitted data, may be disconnected, need to reconnect */
        mqtt_set_client_state(c, CLIENT_STATE_DISCONNECTED);
    }

    RETURN_ERROR(rc);     
}
接收到PUBREC发送PUBREL操作

相关操作请关注22~23行以及40~45行:

该段将PUBREC与PUBREL的操作合并了,在此仅分析接收到PUBREC的操作,22行mqtt_publish_ack_packet执行具体的包交互,操作为回复PUBREL,并且将等待PUBCOMP添加到ack链表中(40~45行),执行完毕后将PUBREC从ack链表中删除。

/**
 * @brief 接收pubrec、pubrel执行回复操作
 * 
 * @param c         结构体句柄
 * @param timer     时间(未使用)
 * @return int 
 */
static int mqtt_pubrec_and_pubrel_packet_handle(mqtt_client_t *c, platform_timer_t *timer)
{
    int rc = MQTT_FAILED_ERROR;
    uint16_t packet_id;
    uint8_t dup, packet_type;
    
    rc = mqtt_is_connected(c);
    if (MQTT_SUCCESS_ERROR != rc)
        RETURN_ERROR(rc);

    if (MQTTDeserialize_ack(&packet_type, &dup, &packet_id, c->mqtt_read_buf, c->mqtt_read_buf_size) != 1)
        RETURN_ERROR(MQTT_PUBREC_PACKET_ERROR);

    (void) dup;
    rc = mqtt_publish_ack_packet(c, packet_id, packet_type);    /* make a ack packet and send it */
    rc = mqtt_ack_list_unrecord(c, packet_type, packet_id, NULL);

    RETURN_ERROR(rc);
}


static int mqtt_publish_ack_packet(mqtt_client_t *c, uint16_t packet_id, int packet_type)
{
    int len = 0;
    int rc = MQTT_SUCCESS_ERROR;
    platform_timer_t timer;
    platform_timer_init(&timer);
    platform_timer_cutdown(&timer, c->mqtt_cmd_timeout);

    platform_mutex_lock(&c->mqtt_write_lock);

    switch (packet_type) {
        case PUBREC:
            len = MQTTSerialize_ack(c->mqtt_write_buf, c->mqtt_write_buf_size, PUBREL, 0, packet_id); /* make a PUBREL ack packet */
            rc = mqtt_ack_list_record(c, PUBCOMP, packet_id, len, NULL);   /* record ack, expect to receive PUBCOMP*/
            if (MQTT_SUCCESS_ERROR != rc)
                goto exit;
            break;
            
        case PUBREL:
            len = MQTTSerialize_ack(c->mqtt_write_buf, c->mqtt_write_buf_size, PUBCOMP, 0, packet_id); /* make a PUBCOMP ack packet */
            break;
            
        default:
            rc = MQTT_PUBLISH_ACK_TYPE_ERROR;
            goto exit;
    }

    if (len <= 0) {
        rc = MQTT_PUBLISH_ACK_PACKET_ERROR;
        goto exit;
    }

    rc = mqtt_send_packet(c, len, &timer);

exit:
    platform_mutex_unlock(&c->mqtt_write_lock);

    RETURN_ERROR(rc);
}
接收到PUBCOMP后操作

相关操作在15行:

收到PUBCOMP后便预示着QOS2通信过程的结束,直接将PUBCOMP剔除ack链表即可。

static int mqtt_puback_and_pubcomp_packet_handle(mqtt_client_t *c, platform_timer_t *timer)
{
    int rc = MQTT_FAILED_ERROR;
    uint16_t packet_id;
    uint8_t dup, packet_type;

    rc = mqtt_is_connected(c);
    if (MQTT_SUCCESS_ERROR != rc)
        RETURN_ERROR(rc);

    if (MQTTDeserialize_ack(&packet_type, &dup, &packet_id, c->mqtt_read_buf, c->mqtt_read_buf_size) != 1)
        rc = MQTT_PUBREC_PACKET_ERROR;
    
    (void) dup;
    rc = mqtt_ack_list_unrecord(c, packet_type, packet_id, NULL);   /* unrecord ack handler */

    RETURN_ERROR(rc);
}
重发机制实现

实现QOS2的核心便是重发机制的实现,kawaii-mqtt通过遍历ack链表,查找所有等待报文中需要重发的类型:PUBACK、PUBREC、PUBREL、PUBCOMP,在链表记录的时间到时执行重发并且更新时间(第23行)。

该重发机制还支持当设备重连时继续进行未完成的协议交互,得益于传参flag,当设备重连时,将会主动调用一次mqtt_ack_list_scan(c,0),此时便会直接将链表中的各重发流程全部执行一次。

/**
 * see if there is a message waiting for the server to answer in the ack list, if there is, then process it according to the flag.
 * flag : 0 means it does not need to wait for the timeout to process these packets immediately. usually immediately after reconnecting.
 *        1 means it needs to wait for timeout before processing these messages, usually timeout processing in a stable connection.
 */
static void mqtt_ack_list_scan(mqtt_client_t* c, uint8_t flag)
{
    mqtt_list_t *curr, *next;
    ack_handlers_t *ack_handler;

    if ((mqtt_list_is_empty(&c->mqtt_ack_handler_list)) || (CLIENT_STATE_CONNECTED != mqtt_get_client_state(c)))
        return;

    LIST_FOR_EACH_SAFE(curr, next, &c->mqtt_ack_handler_list) {
        ack_handler = LIST_ENTRY(curr, ack_handlers_t, list);
        
        if ((!platform_timer_is_expired(&ack_handler->timer)) && (flag == 1))
            continue;
        
        if ((ack_handler->type ==  PUBACK) || (ack_handler->type ==  PUBREC) || (ack_handler->type ==  PUBREL) || (ack_handler->type ==  PUBCOMP)) {
            
            /* timeout has occurred. for qos1 and qos2 packets, you need to resend them. */
            mqtt_ack_handler_resend(c, ack_handler);
            continue;
        } else if ((ack_handler->type == SUBACK) || (ack_handler->type == UNSUBACK)) {
            
            /*@lchnu, 2020-10-08, destory handler memory, if suback/unsuback is overdue!*/
            if (NULL != ack_handler->handler) {
                mqtt_msg_handler_destory(ack_handler->handler);
                ack_handler->handler = NULL;
            }
        }
        /* if it is not a qos1 or qos2 message, it will be destroyed in every processing */
        mqtt_ack_handler_destroy(ack_handler);
        mqtt_subtract_ack_handler_num(c); /*@lchnu, 2020-10-08 */
    }
}

接收QOS2(以项目开发为例)

本段所举例基础为RTT paho-mqtt库,当前该库未支持QOS2的消息安全,因此需要手动实现QOS2消息的接收。

链表操作

本例中实现的链表通过数组来作为存储实例,在链表的操作中通过数组遍历,链表操作的方式,选用此种方式有多种考虑:

  1. 等待报文时机不确定,单纯链表容易加剧碎片化。
  2. 在操作报文时,遍历频繁,使用数组可以加快遍历速度。
  3. 通过数组方便缓存管理,可以通过宏快速调整,不会出现malloc无法申请的情况。
// QOS2消息链表
#define QOS2_MAX_SAVE_ID 100

// 报文标识符链表缓存最小单位节点
typedef struct qos2_save_id{
    rt_slist_t list;                        // 单链表
    unsigned short packetid;                // QOS2:保存报文标识符
    unsigned short used;                    // QOS2:是否插入链表中
}save_id_t;

// 报文标识符存储,缓存与链表均在此,将该结构传入函数中进行处理
typedef struct{
    save_id_t slist_head;                   // 链表头,其后续的next在下方数组中查找
    save_id_t qos_id[QOS2_MAX_SAVE_ID];     // 存储报文标识符的数组,对应地址放入单向链表中并设置used
}qos2_dispose_t;


/**
 * @brief 查找id(报文标识符)是否在链表中
 * 
 * @param msg   链表结构体
 * @param id    要查找的报文标识符
 * @return int  查找结果,1:success 0:err
 */
int find_qos2id_inlist(qos2_dispose_t *msg,unsigned short id)
{
    int i,re = 0;
    for(i = 0; i < QOS2_MAX_SAVE_ID ; i++ )
    {
        if(msg->qos_id[i].packetid == id && msg->qos_id[i].used == 1)
        {
            re ++;
            if(re > 1)
            {
                msg->qos_id[i].used = 0;
                rt_slist_remove(&msg->slist_head.list,&msg->qos_id[i].list);
                re = 1;
            }
        }
    }
    return re;
}

/**
 * @brief 将报文标识符插入链表中
 * 
 * @param msg   链表结构体
 * @param id    要查找的报文标识符
 * @return int 
 */
int set_qos2id_inlist(qos2_dispose_t *msg,unsigned short id)
{
    int i = 0;
    rt_slist_t *list_p = RT_NULL;
    save_id_t *id_list_msg = RT_NULL;
    for(i = 0; i < QOS2_MAX_SAVE_ID ;i ++)
    {
        if(msg->qos_id[i].used == 0) // 还有空余的没有插入链表的元素
        {
            break;
        }
    }
    if(i >= QOS2_MAX_SAVE_ID) // 没有空余的空间
    {
        // 遍历找到链表末尾的地址
        list_p = &msg->slist_head.list;
        while(list_p->next != RT_NULL)
        {
            list_p = rt_slist_next(list_p);
        }

        // 弹出链表尾
        rt_slist_remove(&msg->slist_head.list,list_p);
        // 要插入消息重装到该节点中
        id_list_msg = rt_slist_entry(list_p,save_id_t,list);
        id_list_msg->packetid = id;
        id_list_msg->used = 1;
        // 插入链表头
        rt_slist_insert(&msg->slist_head.list,&id_list_msg->list);
    }
    else
    {
        msg->qos_id[i].packetid = id;
        msg->qos_id[i].used = 1; // 一定要在插入之前将其值赋1
        rt_slist_insert(&msg->slist_head.list,&msg->qos_id[i].list);
    }
    return 0;
}

/**
 * @brief 将报文标识符在链表中删除
 * 
 * @param msg   链表结构体
 * @param id    要查找的报文标识符
 * @return int 
 */
int remove_qos2id_inlist(qos2_dispose_t *msg,unsigned short id)
{
    int i,re = 0;

    for(i = 0; i < QOS2_MAX_SAVE_ID ; i++ )
    {
        if(msg->qos_id[i].packetid == id && msg->qos_id[i].used == 1)
        {
            msg->qos_id[i].used = 0;
            rt_slist_remove(&msg->slist_head.list,&msg->qos_id[i].list);
        }
    }

    return re;
}
接收PUBLISH回复PUBREC操作

相关操作的核心在33~49行:

在接收到消息后,优先判断QOS等级,若是QOS2则对比报文标识符是否在链表中,若不在则将其放入链表,并且向应用层传递,若在则跳过以上操作,直接执行报文回复。

static int MQTT_cycle(MQTTClient *c)
{
    mqtt_data *usr_data = (mqtt_data *)c->user_data;

    // read the socket, see what work is due
    int packet_type = MQTTPacket_readPacket(c);
    int len = 0,
        rc = PAHO_SUCCESS;

    if (packet_type == -1)
    {
        rt_kprintf("[usr]pack type err %d.\n",packet_type);
        rc = PAHO_FAILURE;
        goto exit;
    }

    switch (packet_type)
    {
    ...
    case PUBLISH:
    {
        MQTTString topicName;
        MQTTMessage msg;
        int intQoS;
        if (MQTTDeserialize_publish(&msg.dup, &intQoS, &msg.retained, &msg.id, &topicName,
                                    (unsigned char **)&msg.payload, (int *)&msg.payloadlen, c->readbuf, c->readbuf_size) != 1)
        {
            rt_kprintf("publish err.\n");
            goto exit;
        }
        msg.qos = (enum QoS)intQoS;

        do
        {
            if(msg.qos == QOS2)
            {
                if(find_qos2id_inlist(&usr_data->qos_id_save,msg.id)) // msg.id在单向链表中
                {
                    break;
                }
                else
                {
                    // 插入到链表中,满了便踢掉最旧的
                    set_qos2id_inlist(&usr_data->qos_id_save,msg.id);
                }
            }
            deliverMessage(c, &topicName, &msg);
        } while (0);

        if (msg.qos != QOS0)
        {
            if (msg.qos == QOS1)
                len = MQTTSerialize_ack(c->buf, c->buf_size, PUBACK, 0, msg.id);
            else if (msg.qos == QOS2)
                len = MQTTSerialize_ack(c->buf, c->buf_size, PUBREC, 0, msg.id);
            if (len <= 0)
                rc = PAHO_FAILURE;
            else
                rc = sendPacket(c, len);
            if (rc == PAHO_FAILURE)
                goto exit; // there was a problem
        }
        break;
    }
    ...
    }

exit:
    return rc;
}
接收PUBREL回复PUBCOMP操作

相关操作在31~44行:

收到PUBREL包后回复PUBCOMP,将报文标识符从链表中删除即可。

static int MQTT_cycle(MQTTClient *c)
{
    mqtt_data *usr_data = (mqtt_data *)c->user_data;

    // read the socket, see what work is due
    int packet_type = MQTTPacket_readPacket(c);
    int len = 0,
        rc = PAHO_SUCCESS;

    if (packet_type == -1)
    {
        rt_kprintf("[usr]pack type err %d.\n",packet_type);
        rc = PAHO_FAILURE;
        goto exit;
    }

    switch (packet_type)
    {
	...
    case PUBREL: 
    {
        /***
         * 根据mqtt协议,此时才应该调用回调,但mqtt协议提供了另一种方法来保证QOS2防止多收的处理
         * 检测报文标识符,添加报文标识符管理,新建一个等待链表,该链表要尽可能保证安全,在此采用数组元素作为节点,也可优化查找时的性能
         * 1.接收到PUBLISH,检测其报文标识符是否在等待链表中
         *  1.1 在等待链表中,则跳过发送,回复PUBREC
         *  1.2 不在,则执行回调,并将其放入等待链表,等待PUBREL的到来
         * 2.接收到PUBREL,此时证明报文所有权完全给到client
         * 3.回复PUBCOMP,并将报文标识符在等待链表中删除
        ***/
        unsigned short mypacketid;
        unsigned char dup, type;
        if (MQTTDeserialize_ack(&type, &dup, &mypacketid, c->readbuf, c->readbuf_size) != 1)
            rc = PAHO_FAILURE;
        else if ((len = MQTTSerialize_ack(c->buf, c->buf_size, PUBCOMP, 0, mypacketid)) <= 0)
            rc = PAHO_FAILURE;
        else if ((rc = sendPacket(c, len)) != PAHO_SUCCESS) // send the PUBCOMP packet
            rc = PAHO_FAILURE; // there was a problem
        if (rc == PAHO_FAILURE)
            goto exit; // there was a problem
        else
            // 删除链表中的qos2报文id
            remove_qos2id_inlist(&usr_data->qos_id_save,mypacketid);
        break;
    }
	...
    }

exit:
    return rc;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值