一個高性能、高穩定性的跨平臺MQTT客戶端——mqttclient設計與實現方式

mqttclient設計與實現方式

設計思想

  • 整體採用分層式設計,代碼實現採用異步設計方式,降低耦合。

  • 消息的處理使用回調的方式處理:用戶指定訂閱的主題與指定消息的處理函數

  • 不依賴外部任何文件。

API接口

mqttclient擁有非常簡潔的API接口,參數都是非常簡單的。

API 說明 示例
mqtt_lease() 申請一個mqtt客戶端 mqtt_client_t *client = mqtt_lease();
mqtt_release() 釋放已申請的mqtt客戶端 mqtt_release(client);
mqtt_connect() 與服務器建立連接 mqtt_connect(client);
mqtt_disconnect() 與服務器斷開連接 mqtt_disconnect(client);
mqtt_subscribe() 訂閱主題,參數:主題名字、服務質量、指定當收到主題數據時的處理函數。 mqtt_subscribe(client, “topic”, QOS0, sub_topic_handle);
mqtt_unsubscribe() 取消訂閱指定主題,參數:主題名字 mqtt_unsubscribe(client,
mqtt_publish() 向指定主題發佈數據,參數:主題名字,mqtt_message_t類型的數據內容 mqtt_publish(client, “topic”, &msg);
mqtt_list_subscribe_topic() 列出客戶端已訂閱的主題 mqtt_list_subscribe_topic(client);
mqtt_set_host() 設置要連接的MQTT服務器地址,參數:域名 / 點分十進制的IP地址 mqtt_set_host(client, “www.jiejie01.top”);
mqtt_set_port() 設置要連接的MQTT服務器端口號 mqtt_set_port(client, “1883”);
mqtt_set_ca() 設置要連接的MQTT服務器ca證書 mqtt_set_ca(client, “ca …”);
mqtt_set_user_name() 設置客戶端的用戶名 mqtt_set_user_name(client, “any”);
mqtt_set_password() 設置客戶端的密碼 mqtt_set_password(client, “any”);
mqtt_set_client_id() 設置客戶端的ID mqtt_set_client_id(client, “any”);
mqtt_set_clean_session() 設置在斷開連接後清除會話 mqtt_set_clean_session(client, 1);
mqtt_set_keep_alive_interval() 設置心跳間隔時間(秒) mqtt_set_keep_alive_interval(client, 50);
mqtt_set_cmd_timeout() 設置命令超時時間(毫秒),主要用於socket讀寫超時 mqtt_set_cmd_timeout(client, 5000);
mqtt_set_reconnect_try_duration() 設置重連的時間間隔(毫秒) mqtt_set_reconnect_try_duration(client, 1024);
mqtt_set_read_buf_size() 設置讀數據緩衝區的大小 mqtt_set_read_buf_size(client, 1024);
mqtt_set_write_buf_size() 設置寫數據緩衝區的大小 mqtt_set_write_buf_size(client, 1024);
mqtt_set_will_flag() 設置遺囑標記 mqtt_set_will_flag(client, 1);
mqtt_set_will_options() 設置遺囑的配置信息,指定遺囑主題,服務質量,遺囑保留標記,遺囑內容 mqtt_set_will_options(client, “will_topic”, QOS0, 0, “will_message”);
mqtt_set_version() 設置MQTT協議的版本,默認值是4,MQTT版本爲3.1.1 mqtt_set_version(client, 4);
mqtt_set_reconnect_handler() 設置重連時的回調函數 mqtt_set_reconnect_handler(client, reconnect_handler);
mqtt_set_interceptor_handler() 設置攔截器處理函數,將所有底層數據上報給用戶 mqtt_set_interceptor_handler(client, interceptor_handler);

MQTT客戶端的核心結構

mqtt_client_t 結構

typedef struct mqtt_client {
    char                        *mqtt_client_id;
    char                        *mqtt_user_name;
    char                        *mqtt_password;
    char                        *mqtt_read_buf;
    char                        *mqtt_write_buf;
    char                        *mqtt_host;
    char                        *mqtt_port;
    char                        *mqtt_ca;
    void                        *mqtt_reconnect_data;
    uint16_t                    mqtt_keep_alive_interval;
    uint16_t                    mqtt_packet_id;
    uint32_t                    mqtt_will_flag          : 1;
    uint32_t                    mqtt_clean_session      : 1;
    uint32_t                    mqtt_ping_outstanding   : 2;
    uint32_t                    mqtt_version            : 4;
    uint32_t                    mqtt_ack_handler_number : 24;
    uint32_t                    mqtt_cmd_timeout;
    uint32_t                    mqtt_read_buf_size;
    uint32_t                    mqtt_write_buf_size;
    uint32_t                    mqtt_reconnect_try_duration;
    size_t                      mqtt_client_id_len;
    size_t                      mqtt_user_name_len;
    size_t                      mqtt_password_len;
    mqtt_will_options_t         *mqtt_will_options;
    client_state_t              mqtt_client_state;
    platform_mutex_t            mqtt_write_lock;
    platform_mutex_t            mqtt_global_lock;
    mqtt_list_t                 mqtt_msg_handler_list;
    mqtt_list_t                 mqtt_ack_handler_list;
    network_t                   *mqtt_network;
    platform_thread_t           *mqtt_thread;
    platform_timer_t            mqtt_reconnect_timer;
    platform_timer_t            mqtt_last_sent;
    platform_timer_t            mqtt_last_received;
    reconnect_handler_t         mqtt_reconnect_handler;
    interceptor_handler_t       mqtt_interceptor_handler;
} mqtt_client_t;

該結構主要維護以下內容:

  1. MQTT客戶端連接服務器必要的參數,如客戶端ID mqtt_client_id、用戶名mqtt_user_name、密碼mqtt_password以及客戶端ID長度mqtt_client_id_len、用戶名長度mqtt_user_name_len、密碼長度mqtt_password_len等。

  2. 讀寫數據緩衝區mqtt_read_buf、mqtt_write_buf及其大小的配置mqtt_read_buf_size、mqtt_write_buf_size

  3. 服務器相關的配置信息,如服務器地址mqtt_host、服務器端口號mqtt_port、服務器CA證書mqtt_ca

  4. 一些MQTT客戶端的配置信息:如心跳時間間隔mqtt_keep_alive_interval、MQTT報文標識符mqtt_packet_id、遺囑標記位mqtt_will_flag、清除會話標記mqtt_clean_session、MQTT協議版本mqtt_version、等待應答列表的最大記錄個數mqtt_ack_handler_number等。

  5. 一些其他的配置,如遺囑消息相關的配置mqtt_will_options、客戶端的狀態mqtt_client_state、寫緩衝區的互斥鎖mqtt_write_lock、全局的互斥鎖mqtt_global_lock等。

  6. 命令超時時間mqtt_cmd_timeout(主要是讀寫阻塞時間、等待響應的時間、重連等待時間等)。

  7. 維護消息處理列表mqtt_msg_handler_list,這是mqtt協議必須實現的內容,所有來自服務器的publish報文都會被處理(前提是訂閱了對應的消息,或者設置了攔截器)。

  8. 維護ack鏈表mqtt_ack_handler_list,這是異步實現的核心,所有等待響應的報文都會被掛載到這個鏈表上。

  9. 維護一個網絡組件層mqtt_network,它可以自動選擇數據通道。

  10. 維護一個內部線程mqtt_thread,所有來自服務器的mqtt包都會在內部線程這裏被處理!

  11. 兩個定時器,分別是掉線重連定時器與保活定時器mqtt_reconnect_timer、mqtt_last_sent、mqtt_last_received

  12. 設置掉線重連後告知應用層的回調函數mqtt_reconnect_handler與參數mqtt_reconnect_data

  13. 設置底層的攔截器的回調函數mqtt_interceptor_handler,將所有底層數據上報給應用層。

mqttclient實現

以下是整個框架的實現方式,方便大家更容易理解mqttclient的代碼與設計思想,讓大家能夠修改源碼與使用,還可以提交pr或者issues,開源的世界期待各位大神的參與,感謝!

除此之外以下代碼的記錄機制超時處理機制是非常好的編程思想,大家有興趣一定要看源代碼!

申請一個mqtt客戶端

mqtt_client_t *mqtt_lease(void);
  1. 這個函數的內部通過動態申請內存的方式申請了一個MQTT客戶端結構mqtt_client_t

  2. 調用**_mqtt_init()**函數將其內部的進行了默認的初始化,如申請網絡組件的內存空間、初始化相關的互斥鎖、鏈表等。

釋放已申請的mqtt客戶端

mqtt_release()

回收MQTT客戶端結構mqtt_client_t的內存空間、網絡組件的內存空間、與服務器斷開連接。

設置MQTT客戶端的信息

通過宏定義去統一設置MQTT客戶端結構mqtt_client_t的信息,定義如下:

#define MQTT_CLIENT_SET_DEFINE(name, type, res)         \
    type mqtt_set_##name(mqtt_client_t *c, type t) {    \
        MQTT_ROBUSTNESS_CHECK((c), res);                \
        c->mqtt_##name = t;                             \
        return c->mqtt_##name;                          \
    }

由編譯器預處理得到相關的函數:mqtt_set_xxx()

MQTT_CLIENT_SET_DEFINE(client_id, char*, NULL)
MQTT_CLIENT_SET_DEFINE(user_name, char*, NULL)
MQTT_CLIENT_SET_DEFINE(password, char*, NULL)
MQTT_CLIENT_SET_DEFINE(host, char*, NULL)
MQTT_CLIENT_SET_DEFINE(port, char*, NULL)
MQTT_CLIENT_SET_DEFINE(ca, char*, NULL)
MQTT_CLIENT_SET_DEFINE(reconnect_data, void*, NULL)
MQTT_CLIENT_SET_DEFINE(keep_alive_interval, uint16_t, 0)
MQTT_CLIENT_SET_DEFINE(will_flag, uint32_t, 0)
MQTT_CLIENT_SET_DEFINE(clean_session, uint32_t, 0)
MQTT_CLIENT_SET_DEFINE(version, uint32_t, 0)
MQTT_CLIENT_SET_DEFINE(cmd_timeout, uint32_t, 0)
MQTT_CLIENT_SET_DEFINE(read_buf_size, uint32_t, 0)
MQTT_CLIENT_SET_DEFINE(write_buf_size, uint32_t, 0)
MQTT_CLIENT_SET_DEFINE(reconnect_try_duration, uint32_t, 0)
MQTT_CLIENT_SET_DEFINE(reconnect_handler, reconnect_handler_t, NULL)
MQTT_CLIENT_SET_DEFINE(interceptor_handler, interceptor_handler_t, NULL)

連接服務器

int mqtt_connect(mqtt_client_t* c);

參數只有 mqtt_client_t 類型的指針,連接服務器則是使用非異步的方式設計,因爲必須等待連接上服務器才能進行下一步操作。

過程如下:

  1. 調用底層的連接函數連接上服務器:

    network_connect(c->network);
    
  2. 序列化mqttCONNECT報文並且發送。

    MQTTSerialize_connect(c->write_buf, c->write_buf_size, &connect_data)
    mqtt_send_packet(c, len, &connect_timer)
    
  3. 等待來自服務器的CONNACK報文

    mqtt_wait_packet(c, CONNACK, &connect_timer)
    
  4. 連接成功後創建一個內部線程mqtt_yield_thread,並在合適的時候啓動它:

    /* connect success, and need init mqtt thread */
    c->mqtt_thread= platform_thread_init("mqtt_yield_thread", mqtt_yield_thread, c, MQTT_THREAD_STACK_SIZE, MQTT_THREAD_PRIO, MQTT_THREAD_TICK);
    
    if (NULL != c->mqtt_thread) {
        mqtt_set_client_state(c, CLIENT_STATE_CONNECTED);
        platform_thread_startup(c->mqtt_thread);
        platform_thread_start(c->mqtt_thread);       /* start run mqtt thread */
    }
    
  5. 而對於重連來說則不會重新創建線程,直接改變客戶端狀態爲連接狀態即可:

    mqtt_set_client_state(c, CLIENT_STATE_CONNECTED);
    

訂閱報文

int mqtt_subscribe(mqtt_client_t* c, const char* topic_filter, mqtt_qos_t qos, message_handler_t handler)

訂閱報文使用異步設計來實現的,參數有字符串類型的主題(支持通配符"#" “+”),主題的服務質量,以及收到報文的處理函數`,如不指定則有默認處理函數。

過程如下:

  1. 序列化訂閱報文並且發送給服務器

    MQTTSerialize_subscribe(c->write_buf, c->write_buf_size, 0, mqtt_get_next_packet_id(c), 1, &topic, (int*)&qos)
    mqtt_send_packet(c, len, &timer)
    
  2. 創建對應的消息處理節點,這個消息節點在收到服務器的SUBACK訂閱應答報文後會掛載到消息處理列表mqtt_msg_handler_list

    mqtt_msg_handler_create(topic_filter, qos, handler)
    
  3. 在發送了報文給服務器那就要等待服務器的響應了,先記錄這個等待SUBACK

    mqtt_ack_list_record(c, SUBACK, mqtt_get_next_packet_id(c), len, msg_handler)
    

取消訂閱

int mqtt_unsubscribe(mqtt_client_t* c, const char* topic_filter);

與訂閱報文的邏輯基本差不多的,指定了取消訂閱的主題。

實現過程如下:

  1. 序列化訂閱報文並且發送給服務器

    MQTTSerialize_unsubscribe(c->write_buf, c->write_buf_size, 0, packet_id, 1, &topic)
    mqtt_send_packet(c, len, &timer)
    
  2. 創建對應的消息處理節點,這個消息節點在收到服務器的UNSUBACK取消訂閱應答報文後將消息處理列表mqtt_msg_handler_list上的已經訂閱的主題消息節點銷燬

    mqtt_msg_handler_create((const char*)topic_filter, QOS0, NULL)
    
  3. 在發送了報文給服務器那就要等待服務器的響應了,先記錄這個等待UNSUBACK

    mqtt_ack_list_record(c, UNSUBACK, packet_id, len, msg_handler)
    

發佈報文

int mqtt_publish(mqtt_client_t* c, const char* topic_filter, mqtt_message_t* msg)

向指定主題發佈一個MQTT報文。參數只有mqtt_client_t 類型的指針,字符串類型的主題(支持通配符),要發佈的消息(包括服務質量消息主體)。

使用如下:

mqtt_message_t msg;

msg.qos = 2;
msg.payload = (void *) buf;

mqtt_publish(&client, "testtopic1", &msg);

代碼的實現核心思想都差不多,過程如下:

  1. 先序列化發佈報文,然後發送到服務器

    MQTTSerialize_publish(c->write_buf, c->write_buf_size, 0, msg->qos, msg->retained, msg->id,topic, (unsigned char*)msg->payload, msg->payloadlen);
    mqtt_send_packet(c, len, &timer)
    
  2. 對於QOS0的邏輯,不做任何處理,對於QOS1和QOS2的報文則需要記錄下來,在沒收到服務器應答的時候進行重發

    if (QOS1 == msg->qos) {
        rc = mqtt_ack_list_record(c, PUBACK, mqtt_get_next_packet_id(c), len, NULL);
    } else if (QOS2 == msg->qos) {
        rc = mqtt_ack_list_record(c, PUBREC, mqtt_get_next_packet_id(c), len, NULL);
    }
    
  3. 還有非常重要的一點,重發報文的MQTT報文頭部需要設置DUP標誌位,這是MQTT協議的標準,因此,在重發的時候作者直接操作了報文的DUP標誌位,因爲修改DUP標誌位的函數我沒有從MQTT庫中找到,所以我封裝了一個函數,這與LwIP中的交叉存取思想是一個道理,它假設我知道MQTT報文的所有操作,所以我可以操作它,這樣子可以提高很多效率:

    mqtt_set_publish_dup(c,1);  /* may resend this data, set the udp flag in advance */
    

內部線程

static void mqtt_yield_thread(void *arg)

主要是對mqtt_yield函數的返回值做處理,比如在disconnect的時候銷燬這個線程。

核心的處理函數

  1. 數據包的處理mqtt_packet_handle

    static int mqtt_packet_handle(mqtt_client_t* c, platform_timer_t* timer)
    

    對不同的包使用不一樣的處理:

    switch (packet_type) {
        case 0: /* timed out reading packet */
            break;
    
        case CONNACK:
            break;
    
        case PUBACK:
        case PUBCOMP:
            rc = mqtt_puback_and_pubcomp_packet_handle(c, timer);
            break;
    
        case SUBACK:
            rc = mqtt_suback_packet_handle(c, timer);
            break;
            
        case UNSUBACK:
            rc = mqtt_unsuback_packet_handle(c, timer);
            break;
    
        case PUBLISH:
            rc = mqtt_publish_packet_handle(c, timer);
            break;
    
        case PUBREC:
        case PUBREL:
            rc = mqtt_pubrec_and_pubrel_packet_handle(c, timer);
            break;
    
        case PINGRESP:
            c->ping_outstanding = 0;
            break;
    
        default:
            goto exit;
    }
    

    並且做保活的處理:

    mqtt_keep_alive(c)
    

    當發生超時後的處理:

    if (platform_timer_is_expired(&c->last_sent) || platform_timer_is_expired(&c->last_received)) 
    

    序列化一個心跳包並且發送給服務器

    MQTTSerialize_pingreq(c->write_buf, c->write_buf_size);
    mqtt_send_packet(c, len, &timer);
    

    當再次發生超時後,表示與服務器的連接已斷開,需要重連的操作,設置客戶端狀態爲斷開連接

    mqtt_set_client_state(c, CLIENT_STATE_DISCONNECTED);
    
  2. ack鏈表的掃描,當收到服務器的報文時,對ack列表進行掃描操作

    mqtt_ack_list_scan(c);
    

    當超時後就銷燬ack鏈表節點:

    mqtt_ack_handler_destroy(ack_handler);
    

    當然下面這幾種報文則需要重發操作:(PUBACK 、PUBREC、 PUBREL 、PUBCOMP,保證QOS1 QOS2的服務質量)

    if ((ack_handler->type ==  PUBACK) || (ack_handler->type ==  PUBREC) || (ack_handler->type ==  PUBREL) || (ack_handler->type ==  PUBCOMP))
        mqtt_ack_handler_resend(c, ack_handler);
    
  3. 保持活性的時間過去了,可能掉線了,需要重連操作

    mqtt_try_reconnect(c);
    

    重連成功後嘗試重新訂閱報文,保證恢復原始狀態~

    mqtt_try_resubscribe(c)
    

發佈應答與發佈完成報文的處理

static int mqtt_puback_and_pubcomp_packet_handle(mqtt_client_t *c, platform_timer_t *timer)
  1. 反序列化報文

    MQTTDeserialize_ack(&packet_type, &dup, &packet_id, c->read_buf, c->read_buf_size)
    
  2. 取消對應的ack記錄

    mqtt_ack_list_unrecord(c, packet_type, packet_id, NULL);
    

訂閱應答報文的處理

```c
static int mqtt_suback_packet_handle(mqtt_client_t *c, platform_timer_t *timer)
```
  1. 反序列化報文

    MQTTDeserialize_suback(&packet_id, 1, &count, (int*)&granted_qos, c->read_buf, c->read_buf_size)
    
  2. 取消對應的ack記錄

    mqtt_ack_list_unrecord(c, packet_type, packet_id, NULL);
    
  3. 安裝對應的訂閱消息處理函數,如果是已存在的則不會安裝

    mqtt_msg_handlers_install(c, msg_handler);
    

取消訂閱應答報文的處理

```c
static int mqtt_unsuback_packet_handle(mqtt_client_t *c, platform_timer_t *timer)
```
  1. 反序列化報文

    MQTTDeserialize_unsuback(&packet_id, c->read_buf, c->read_buf_size)
    
  2. 取消對應的ack記錄,並且獲取到已經訂閱的消息處理節點

    mqtt_ack_list_unrecord(c, UNSUBACK, packet_id, &msg_handler)
    
  3. 銷燬對應的訂閱消息處理函數

    mqtt_msg_handler_destory(msg_handler);
    

來自服務器的發佈報文的處理

```c
static int mqtt_publish_packet_handle(mqtt_client_t *c, platform_timer_t *timer)
```
  1. 反序列化報文

    MQTTDeserialize_publish(&msg.dup, &qos, &msg.retained, &msg.id, &topic_name,
            (unsigned char**)&msg.payload, (int*)&msg.payloadlen, c->read_buf, c->read_buf_size)
    
  2. 對於QOS0、QOS1的報文,直接去處理消息

    mqtt_deliver_message(c, &topic_name, &msg);
    
  3. 對於QOS1的報文,還需要發送一個PUBACK應答報文給服務器

    MQTTSerialize_ack(c->write_buf, c->write_buf_size, PUBACK, 0, msg.id);
    
  4. 而對於QOS2的報文則需要發送PUBREC報文給服務器,除此之外還需要記錄PUBREL到ack鏈表上,等待服務器的發佈釋放報文,最後再去處理這個消息

    MQTTSerialize_ack(c->write_buf, c->write_buf_size, PUBREC, 0, msg.id);
    mqtt_ack_list_record(c, PUBREL, msg.id + 1, len, NULL)
    mqtt_deliver_message(c, &topic_name, &msg);
    

說明:一旦註冊到ack列表上的報文,當具有重複的報文是不會重新被註冊的,它會通過**mqtt_ack_list_node_is_exist()**函數判斷這個節點是否存在,主要是依賴等待響應的消息類型與msgid。

發佈收到與發佈釋放報文的處理

static int mqtt_pubrec_and_pubrel_packet_handle(mqtt_client_t *c, platform_timer_t *timer)
  1. 反序列化報文

    MQTTDeserialize_ack(&packet_type, &dup, &packet_id, c->read_buf, c->read_buf_size)
    
  2. 產生一個對應的應答報文

    mqtt_publish_ack_packet(c, packet_id, packet_type);
    
  3. 取消對應的ack記錄

    mqtt_ack_list_unrecord(c, UNSUBACK, packet_id, &msg_handler)
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章