一個基於socket API之上的跨平臺MQTT客戶端,支持qos2

mqttclient

一個基於socket API之上的跨平臺MQTT客戶端

源碼地址 https://github.com/jiejieTop/mqttclient

整體框架

整體框架

備註:目前只實現了linux平臺,TencentOS tiny與RT-Thread正在移植中

linux平臺下測試使用

安裝cmake:

sudo apt-get install cmake

配置

mqttclient/test/test.c文件中修改以下內容:

    init_params.connect_params.network_params.network_ssl_params.ca_crt = test_ca_get();    /* CA證書 */
    init_params.connect_params.network_params.addr = "xxxxxxx";                             /* 服務器域名 */
    init_params.connect_params.network_params.port = "8883";
    init_params.connect_params.user_name = "xxxxxxx";                                       /* 用戶名 */
    init_params.connect_params.password = "xxxxxxx";                                        /* 密碼 */
    init_params.connect_params.client_id = "xxxxxxx";                                       /* 客戶端id */

打開salof

salof 全稱是:Synchronous Asynchronous Log Output Framework(同步異步日誌輸出框架)

它是一個異步日誌輸出庫,在空閒時候輸出對應的日誌信息,並且該庫與mqttclient無縫銜接,如果不需要則將 LOG_IS_SALOF 定義爲0即可。

#define LOG_IS_SALOF    0

mqttclient/common/log/config.h配置文件中打開對應的日誌輸出級別:

#define BASE_LEVEL      (0)
#define ASSERT_LEVEL    (BASE_LEVEL + 1)            /* 日誌輸出級別:斷言級別(非常高優先級) */
#define ERR_LEVEL       (ASSERT_LEVEL + 1)          /* 日誌輸出級別:錯誤級別(高優先級) */
#define WARN_LEVEL      (ERR_LEVEL + 1)             /* 日誌輸出級別:警告級別(中優先級) */
#define INFO_LEVEL      (WARN_LEVEL + 1)            /* 日誌輸出級別:信息級別(低優先級) */
#define DEBUG_LEVEL     (INFO_LEVEL + 1)            /* 日誌輸出級別:調試級別(更低優先級) */

#define         SALOF_OS                    USE_LINUX       /* 選擇對應的平臺:Linux/FreeRTOS/TencentOS */
#define         LOG_LEVEL                   WARN_LEVEL      /* 日誌輸出級別 */

mqttclient的配置

配置文件是:mqttclient/mqtt_config.h,在這裏可以根據自身需求配置對應的信息。
如是否選擇mbedtls加密層:

#define     MQTT_NETWORK_TYPE_TLS               MQTT_YES

編譯 & 運行

./build.sh

運行build.sh腳本後會在 ./build/bin/目錄下生成可執行文件mqtt-client,直接運行即可。

設計思想

  • 整體採用分層式設計,代碼實現採用異步設計方式,降低耦合。
  • 消息的處理使用回調的方式處理:用戶指定[訂閱的主題]與指定[消息的處理函數]
  • 不對外產生依賴

API

mqttclient擁有非常簡潔的api接口

int mqtt_keep_alive(mqtt_client_t* c);
int mqtt_init(mqtt_client_t* c, client_init_params_t* init);
int mqtt_release(mqtt_client_t* c);
int mqtt_connect(mqtt_client_t* c);
int mqtt_disconnect(mqtt_client_t* c);
int mqtt_subscribe(mqtt_client_t* c, const char* topic_filter, mqtt_qos_t qos, message_handler_t msg_handler);
int mqtt_unsubscribe(mqtt_client_t* c, const char* topic_filter);
int mqtt_publish(mqtt_client_t* c, const char* topic_filter, mqtt_message_t* msg);
int mqtt_yield(mqtt_client_t* c, int timeout_ms);

核心

mqtt_client_t 結構

typedef struct mqtt_client {
    unsigned short              packet_id;
    unsigned char               *read_buf;
    unsigned char               *write_buf;
    unsigned char               ping_outstanding;
    unsigned char               ack_handler_number;
    unsigned int                cmd_timeout;
    unsigned int                read_buf_size;
    unsigned int                write_buf_size;
    unsigned int                reconnect_try_duration;
    void                        *reconnect_date;
    reconnect_handler_t         reconnect_handler;
    client_state_t              client_state;
    platform_mutex_t            write_lock;
    platform_mutex_t            global_lock;
    list_t                      msg_handler_list;
    list_t                      ack_handler_list;
    network_t                   *network;
    platform_thread_t           *thread;
    platform_timer_t            reconnect_timer;
    platform_timer_t            ping_timer;
    connect_params_t            *connect_params;
} mqtt_client_t;

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

  1. 讀寫數據緩衝區read_buf、write_buf
  2. 命令超時時間cmd_timeout(主要是讀寫阻塞時間、等待響應的時間、重連等待時間)
  3. 維護ack鏈表ack_handler_list,這是異步實現的核心,所有等待響應的報文都會被掛載到這個鏈表上
  4. 維護消息處理列表msg_handler_list,這是mqtt協議必須實現的內容,所有來自服務器的publish報文都會被處理(前提是訂閱了對應的消息)
  5. 維護一個網卡接口network
  6. 維護一個內部線程thread,所有來自服務器的mqtt包都會在這裏被處理!
  7. 兩個定時器,分別是掉線重連定時器與保活定時器reconnect_timer、ping_timer
  8. 一些連接的參數connect_params

初始化

int mqtt_init(mqtt_client_t* c, client_init_params_t* init)

主要是配置mqtt_client_t結構的相關信息,如果沒有指定初始化參數,則系統會提供默認的參數。
但連接部分的參數則必須指定:

    init_params.connect_params.network_params.addr = "[你的mqtt服務器IP地址或者是域名]";
    init_params.connect_params.network_params.port = 1883;	//端口號
    init_params.connect_params.user_name = "jiejietop";
    init_params.connect_params.password = "123456";
    init_params.connect_params.client_id = "clientid";

連接服務器

int mqtt_connect(mqtt_client_t* c);

連接服務器則是使用非異步的方式設計,因爲必須等待連接上服務器才能進行下一步操作。
過程如下

  1. 調用底層的連接函數連接上服務器:
c->network->connect(c->network);
  1. 序列化mqttCONNECT報文並且發送
MQTTSerialize_connect(c->write_buf, c->write_buf_size, &connect_data)
mqtt_send_packet(c, len, &connect_timer)
  1. 等待來自服務器的CONNACK報文
mqtt_wait_packet(c, CONNACK, &connect_timer)
  1. 連接成功後創建一個內部線程mqtt_yield_thread
platform_thread_init("mqtt_yield_thread", mqtt_yield_thread, c, MQTT_THREAD_STACK_SIZE, MQTT_THREAD_PRIO, MQTT_THREAD_TICK)

訂閱報文

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)
  1. 創建對應的消息處理節點,這個消息節點在收到服務器的SUBACK訂閱應答報文後會掛載到消息處理列表msg_handler_list
mqtt_msg_handler_create(topic_filter, qos, handler)
  1. 在發送了報文給服務器那就要等待服務器的響應了,記錄這個等待SUBACK
mqtt_ack_list_record(c, SUBACK, mqtt_get_next_packet_id(c), len, msg_handler)

取消訂閱

與訂閱報文的邏輯基本差不多的~

發佈報文

int mqtt_publish(mqtt_client_t* c, const char* topic_filter, mqtt_message_t* 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)
  1. 對於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);
    }

內部線程

static void mqtt_yield_thread(void *arg)

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

核心的處理函數mqtt_yield

  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)
  1. 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);
  1. 保持活性的時間過去了,可能掉線了,需要重連操作
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)
  1. 取消對應的ack記錄
mqtt_ack_list_unrecord(c, packet_type, packet_id, NULL);

訂閱應答報文的處理

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)
  1. 取消對應的ack記錄
mqtt_ack_list_unrecord(c, packet_type, packet_id, NULL);
  1. 安裝對應的訂閱消息處理函數,如果是已存在的則不會安裝
mqtt_msg_handlers_install(c, msg_handler);

取消訂閱應答報文的處理

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)
  1. 取消對應的ack記錄
mqtt_ack_list_unrecord(c, UNSUBACK, packet_id, &msg_handler)
  1. 銷燬對應的訂閱消息處理函數
mqtt_msg_handler_destory(msg_handler);

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

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)
  1. 對於QOS0、QOS1的報文,直接去處理消息
mqtt_deliver_message(c, &topic_name, &msg);
  1. 對於QOS1的報文,還需要發送一個PUBACK應答報文給服務器
MQTTSerialize_ack(c->write_buf, c->write_buf_size, PUBACK, 0, msg.id);
  1. 而對於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)
  1. 產生一個對應的應答報文
mqtt_publish_ack_packet(c, packet_id, packet_type);
  1. 取消對應的ack記錄
mqtt_ack_list_unrecord(c, UNSUBACK, packet_id, &msg_handler)

源碼地址 https://github.com/jiejieTop/mqttclient

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