文章目錄
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;
該結構主要維護以下內容:
-
MQTT客戶端連接服務器必要的參數,如客戶端ID mqtt_client_id、用戶名mqtt_user_name、密碼mqtt_password以及客戶端ID長度mqtt_client_id_len、用戶名長度mqtt_user_name_len、密碼長度mqtt_password_len等。
-
讀寫數據緩衝區mqtt_read_buf、mqtt_write_buf及其大小的配置mqtt_read_buf_size、mqtt_write_buf_size。
-
服務器相關的配置信息,如服務器地址mqtt_host、服務器端口號mqtt_port、服務器CA證書mqtt_ca。
-
一些MQTT客戶端的配置信息:如心跳時間間隔mqtt_keep_alive_interval、MQTT報文標識符mqtt_packet_id、遺囑標記位mqtt_will_flag、清除會話標記mqtt_clean_session、MQTT協議版本mqtt_version、等待應答列表的最大記錄個數mqtt_ack_handler_number等。
-
一些其他的配置,如遺囑消息相關的配置mqtt_will_options、客戶端的狀態mqtt_client_state、寫緩衝區的互斥鎖mqtt_write_lock、全局的互斥鎖mqtt_global_lock等。
-
命令超時時間mqtt_cmd_timeout(主要是讀寫阻塞時間、等待響應的時間、重連等待時間等)。
-
維護消息處理列表mqtt_msg_handler_list,這是mqtt協議必須實現的內容,所有來自服務器的publish報文都會被處理(前提是訂閱了對應的消息,或者設置了攔截器)。
-
維護ack鏈表mqtt_ack_handler_list,這是異步實現的核心,所有等待響應的報文都會被掛載到這個鏈表上。
-
維護一個網絡組件層mqtt_network,它可以自動選擇數據通道。
-
維護一個內部線程mqtt_thread,所有來自服務器的mqtt包都會在內部線程這裏被處理!
-
兩個定時器,分別是掉線重連定時器與保活定時器mqtt_reconnect_timer、mqtt_last_sent、mqtt_last_received
-
設置掉線重連後告知應用層的回調函數mqtt_reconnect_handler與參數mqtt_reconnect_data。
-
設置底層的攔截器的回調函數mqtt_interceptor_handler,將所有底層數據上報給應用層。
mqttclient實現
以下是整個框架的實現方式,方便大家更容易理解mqttclient的代碼與設計思想,讓大家能夠修改源碼與使用,還可以提交pr或者issues,開源的世界期待各位大神的參與,感謝!
除此之外以下代碼的記錄機制與超時處理機制是非常好的編程思想,大家有興趣一定要看源代碼!
申請一個mqtt客戶端
mqtt_client_t *mqtt_lease(void);
-
這個函數的內部通過動態申請內存的方式申請了一個MQTT客戶端結構mqtt_client_t。
-
調用**_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 類型的指針,連接服務器則是使用非異步的方式設計,因爲必須等待連接上服務器才能進行下一步操作。
過程如下:
-
調用底層的連接函數連接上服務器:
network_connect(c->network);
-
序列化mqtt的CONNECT報文並且發送。
MQTTSerialize_connect(c->write_buf, c->write_buf_size, &connect_data) mqtt_send_packet(c, len, &connect_timer)
-
等待來自服務器的CONNACK報文
mqtt_wait_packet(c, CONNACK, &connect_timer)
-
連接成功後創建一個內部線程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 */ }
-
而對於重連來說則不會重新創建線程,直接改變客戶端狀態爲連接狀態即可:
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)
訂閱報文使用異步設計來實現的,參數有字符串類型的主題(支持通配符"#" “+”),主題的服務質量,以及收到報文的處理函數`,如不指定則有默認處理函數。
過程如下:
-
序列化訂閱報文並且發送給服務器
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)
-
創建對應的消息處理節點,這個消息節點在收到服務器的SUBACK訂閱應答報文後會掛載到消息處理列表mqtt_msg_handler_list上
mqtt_msg_handler_create(topic_filter, qos, handler)
-
在發送了報文給服務器那就要等待服務器的響應了,先記錄這個等待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);
與訂閱報文的邏輯基本差不多的,指定了取消訂閱的主題。
實現過程如下:
-
序列化訂閱報文並且發送給服務器
MQTTSerialize_unsubscribe(c->write_buf, c->write_buf_size, 0, packet_id, 1, &topic) mqtt_send_packet(c, len, &timer)
-
創建對應的消息處理節點,這個消息節點在收到服務器的UNSUBACK取消訂閱應答報文後將消息處理列表mqtt_msg_handler_list上的已經訂閱的主題消息節點銷燬
mqtt_msg_handler_create((const char*)topic_filter, QOS0, NULL)
-
在發送了報文給服務器那就要等待服務器的響應了,先記錄這個等待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);
代碼的實現核心思想都差不多,過程如下:
-
先序列化發佈報文,然後發送到服務器
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)
-
對於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); }
-
還有非常重要的一點,重發報文的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的時候銷燬這個線程。
核心的處理函數
-
數據包的處理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);
-
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);
-
保持活性的時間過去了,可能掉線了,需要重連操作
mqtt_try_reconnect(c);
重連成功後嘗試重新訂閱報文,保證恢復原始狀態~
mqtt_try_resubscribe(c)
發佈應答與發佈完成報文的處理
static int mqtt_puback_and_pubcomp_packet_handle(mqtt_client_t *c, platform_timer_t *timer)
-
反序列化報文
MQTTDeserialize_ack(&packet_type, &dup, &packet_id, c->read_buf, c->read_buf_size)
-
取消對應的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)
```
-
反序列化報文
MQTTDeserialize_suback(&packet_id, 1, &count, (int*)&granted_qos, c->read_buf, c->read_buf_size)
-
取消對應的ack記錄
mqtt_ack_list_unrecord(c, packet_type, packet_id, NULL);
-
安裝對應的訂閱消息處理函數,如果是已存在的則不會安裝
mqtt_msg_handlers_install(c, msg_handler);
取消訂閱應答報文的處理
```c
static int mqtt_unsuback_packet_handle(mqtt_client_t *c, platform_timer_t *timer)
```
-
反序列化報文
MQTTDeserialize_unsuback(&packet_id, c->read_buf, c->read_buf_size)
-
取消對應的ack記錄,並且獲取到已經訂閱的消息處理節點
mqtt_ack_list_unrecord(c, UNSUBACK, packet_id, &msg_handler)
-
銷燬對應的訂閱消息處理函數
mqtt_msg_handler_destory(msg_handler);
來自服務器的發佈報文的處理
```c
static int mqtt_publish_packet_handle(mqtt_client_t *c, platform_timer_t *timer)
```
-
反序列化報文
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)
-
對於QOS0、QOS1的報文,直接去處理消息
mqtt_deliver_message(c, &topic_name, &msg);
-
對於QOS1的報文,還需要發送一個PUBACK應答報文給服務器
MQTTSerialize_ack(c->write_buf, c->write_buf_size, PUBACK, 0, msg.id);
-
而對於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)
-
反序列化報文
MQTTDeserialize_ack(&packet_type, &dup, &packet_id, c->read_buf, c->read_buf_size)
-
產生一個對應的應答報文
mqtt_publish_ack_packet(c, packet_id, packet_type);
-
取消對應的ack記錄
mqtt_ack_list_unrecord(c, UNSUBACK, packet_id, &msg_handler)