本文轉自:http://chenzhenianqing.cn/articles/977.html
Mosquitto是一個IBM 開源pub/sub訂閱發佈協議MQTT的一個單機版實現(目前也只有單機版),MQTT主打輕便,比較適用於移動設備等上面,花費流量少,解析代價低。相對於XMPP等來說,簡單許多。
MQTT採用二進制協議,而不是XMPP的XML協議,所以一般消息甚至只需要花費2個字節的大小就可以交換信息了,對於移動開發比較有優勢。
IBM雖然開源了其MQTT消息協議,但是卻沒有開源其RSMB服務端程序,不過還好目前有比較穩定的實現可用,本文的Mosquitto是其中比較活躍的實現之一,具體在這裏有目前的實現列表可供選擇。
趁着大腦還沒有進入睡眠狀態記錄一下剛纔看代碼學到的東西。我下載的版本是1.2.2版,在這裏可以找到下載鏈接。
零、介紹
關於MQTT 3.1協議本身比較簡單,42頁的PDF介紹完了,相比XMPP那長長的文檔,謝天謝地了。由於剛看,所以很多細節都沒有深入進去,這裏只是記錄個大概,後續有時間慢慢補好坑吧。
總體來說,mosquitto實現有如下幾個特點:
- poll()異步模型,竟然不是epoll,也許這注定了其只能支持十幾萬連接同時在線的悲劇吧。
- 內存處理方面幾乎沒有任何優化,但簡單可依賴;
- 多線程程序,許多地方都得加鎖訪問。但其實多線程的需求沒那麼強烈,可以考慮避免;
總之,是一個比較簡單單可以適用於一般的服務中提供pub/sub功能支持,但如果放到大量併發的系統中,可以優化的地方還有很多。關於mosquitto的性能,暫時沒有找到官方的評測,不過在郵件組裏面找到的一些討論似乎顯示其性能上限爲20W連接時在線的狀態,當然具體取決於業務邏輯,交互是否很多等。不過這樣的成績還是不錯的。一臺機器可以起多個實例的嘛。
一、初始化
mosquitto.c文件main開頭調用_mosquitto_net_init初始化SSL加密的庫,然後調用mqtt3_config_init初始化配置的各個數據結構爲默認值。配置文件的解析由mqtt3_config_parse_args牽頭完成,具體配置文件解析就不多寫了,fgets一行行的讀取配置,然後設置到config全局變量中。其中包括對於監聽地址等的讀取。
然後保存pid進程號。mqtt3_db_open打開db文件
1 |
int main( int argc, char *argv[]) |
4 |
memset (&int_db,
0, sizeof ( struct mosquitto_db)); |
8 |
mqtt3_config_init(&config); |
9 |
rc
= mqtt3_config_parse_args(&config, argc, argv); |
配置讀取完後,就可以打開監聽端口了,使用mqtt3_socket_listen打開監聽端口,並將SOCK套接字放在局部變量listensock裏面,以便後面統一使用。
3 |
for (i=0;
i<config.listener_count; i++){ |
4 |
if (mqtt3_socket_listen(&config.listeners[i])){ |
5 |
_mosquitto_free(int_db.contexts); |
6 |
mqtt3_db_close(&int_db); |
8 |
remove (config.pid_file); |
12 |
listensock_count
+= config.listeners[i].sock_count; |
13 |
listensock
= _mosquitto_realloc(listensock, sizeof ( int )*listensock_count); |
15 |
_mosquitto_free(int_db.contexts); |
16 |
mqtt3_db_close(&int_db); |
18 |
remove (config.pid_file); |
22 |
for (j=0;
j<config.listeners[i].sock_count; j++){ |
23 |
if (config.listeners[i].socks[j]
== INVALID_SOCKET){ |
24 |
_mosquitto_free(int_db.contexts); |
25 |
mqtt3_db_close(&int_db); |
27 |
remove (config.pid_file); |
31 |
listensock[listensock_index]
= config.listeners[i].socks[j]; |
32 |
if (listensock[listensock_index]
> listener_max){ |
33 |
listener_max
= listensock[listensock_index]; |
關於mqtt3_socket_listen函數也比較經典,socket(),bind(), listen()的流程,不同的是使用了新版的套接字信息獲取函數getaddrinfo,該函數支持IPV4和IPV6,對應用層透明,不需要處理這些信息。
1 |
int mqtt3_socket_listen( struct _mqtt3_listener
*listener) |
3 |
snprintf(service,
10, "%d" ,
listener->port); |
4 |
memset (&hints,
0, sizeof ( struct addrinfo)); |
5 |
hints.ai_family
= PF_UNSPEC; |
6 |
hints.ai_flags
= AI_PASSIVE; |
7 |
hints.ai_socktype
= SOCK_STREAM; |
12 |
if (getaddrinfo(listener->host,
service, &hints, &ainfo)) return INVALID_SOCKET; |
14 |
listener->sock_count
= 0; |
15 |
listener->socks
= NULL; |
17 |
for (rp
= ainfo; rp; rp = rp->ai_next){ |
19 |
sock
= socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); |
21 |
strerror_r( errno ,
err, 256); |
22 |
_mosquitto_log_printf(NULL,
MOSQ_LOG_WARNING, "Warning:
%s" ,
err); |
25 |
listener->sock_count++; |
26 |
listener->socks
= _mosquitto_realloc(listener->socks, sizeof ( int )*listener->sock_count); |
28 |
_mosquitto_log_printf(NULL,
MOSQ_LOG_ERR, "Error:
Out of memory." ); |
29 |
return MOSQ_ERR_NOMEM; |
31 |
listener->socks[listener->sock_count-1]
= sock; |
33 |
opt
= fcntl(sock, F_GETFL, 0); |
35 |
if (bind(sock,
rp->ai_addr, rp->ai_addrlen) == -1){ |
36 |
strerror_r( errno ,
err, 256); |
37 |
_mosquitto_log_printf(NULL,
MOSQ_LOG_ERR, "Error:
%s" ,
err); |
42 |
if (listen(sock,
100) == -1){ |
43 |
strerror_r( errno ,
err, 256); |
44 |
_mosquitto_log_printf(NULL,
MOSQ_LOG_ERR, "Error:
%s" ,
err); |
二、消息事件循環
打開監聽套接字後,就可以進入消息事件循環,標準網絡服務程序的必須過程。這個由main函數調用mosquitto_main_loop啓動。mosquitto_main_loop函數主體也是一個大循環,在循環裏面進行超時檢測,事件處理,網絡讀寫等等。由於使用poll模型,所以就需要在進行poll()等待之前準備需要監聽的套接字數組列表pollfds,效率低的地方就在這裏。
對於監聽套接字,簡單將其加入pollfds裏面,註冊POLLIN可讀事件即可。如果對於其他跟客戶端等的連接,就需要多做一步操作了。如果是橋接模式,進行相應的處理,這裏暫時不介紹橋接模式,橋接模式是爲了分佈式部署加入的非標準協議,目前只有IBM rsmb和mosquitto實現了。
對於跟客戶端的連接,mosquitto會在poll等待之前調用mqtt3_db_message_write嘗試發送一次未發送的數據給對方,避免不必要的等待可能。
1 |
int mosquitto_main_loop( struct mosquitto_db
*db, int *listensock, int listensock_count, int listener_max) |
3 |
memset (pollfds,
-1, sizeof ( struct pollfd)*pollfd_count); |
6 |
for (i=0;
i<listensock_count; i++){ |
7 |
pollfds[pollfd_index].fd
= listensock[i]; |
8 |
pollfds[pollfd_index].events
= POLLIN; |
9 |
pollfds[pollfd_index].revents
= 0; |
14 |
for (i=0;
i<db->context_count; i++){ |
19 |
if (!(db->contexts[i]->keepalive) |
20 |
||
db->contexts[i]->bridge |
21 |
||
now - db->contexts[i]->last_msg_in < ( time_t )(db->contexts[i]->keepalive)*3/2){ |
24 |
if (mqtt3_db_message_write(db->contexts[i])
== MOSQ_ERR_SUCCESS){ |
25 |
pollfds[pollfd_index].fd
= db->contexts[i]->sock; |
26 |
pollfds[pollfd_index].events
= POLLIN | POLLRDHUP; |
27 |
pollfds[pollfd_index].revents
= 0; |
28 |
if (db->contexts[i]->current_out_packet){ |
29 |
pollfds[pollfd_index].events
|= POLLOUT; |
31 |
db->contexts[i]->pollfd_index
= pollfd_index; |
34 |
mqtt3_context_disconnect(db,
db->contexts[i]); |
37 |
if (db->config->connection_messages
== true ){ |
38 |
_mosquitto_log_printf(NULL,
MOSQ_LOG_NOTICE, "Client
%s has exceeded timeout, disconnecting." ,
db->co |
42 |
mqtt3_context_disconnect(db,
db->contexts[i]); |
46 |
if (db->contexts[i]->clean_session
== true ){ |
48 |
mqtt3_context_cleanup(db,
db->contexts[i], true ); |
49 |
db->contexts[i]
= NULL; |
50 |
} else if (db->config->persistent_client_expiration
> 0){ |
58 |
if (now
> db->contexts[i]->disconnect_t+db->config->persistent_client_expiration){ |
59 |
_mosquitto_log_printf(NULL,
MOSQ_LOG_NOTICE, "Expiring
persistent client %s due to timeout." ,
db- |
64 |
db->contexts[i]->clean_session
= true ; |
65 |
mqtt3_context_cleanup(db,
db->contexts[i], true ); |
66 |
db->contexts[i]
= NULL; |
然後先使用mqtt3_db_message_timeout_check檢測一下超時沒有收到客戶端回包確認的消息,mosquitto對於超時的消息處理,是會進行重發的。不過理論上,TCP是不需要重發的,具體見這裏:MQTT消息推送協議應用數據包超時是否需要重發? 不過,由於mosquitto對於客戶端斷開連接的處理比較弱,連接重新建立後,使用的相關數據結構還是相同的,因此重發其實也可以,只是這個時候的重發,實際上是在一個連接上沒有收到ACK回包,然後後續建立的新連接上進行重傳。不是在一個連接上重傳。但是這樣其實也有很多弊端,比如客戶端必須支持消息的持久化記錄,否則容易雙方對不上話的情況。
1 |
int mqtt3_db_message_timeout_check( struct mosquitto_db
*db, unsigned int timeout) |
5 |
enum mosquitto_msg_state
new_state = mosq_ms_invalid; |
6 |
struct mosquitto
*context; |
7 |
struct mosquitto_client_msg
*msg; |
9 |
threshold
= mosquitto_time() - timeout; |
11 |
for (i=0;
i<db->context_count; i++){ |
12 |
context
= db->contexts[i]; |
13 |
if (!context) continue ; |
20 |
if (msg->timestamp
< threshold && msg->state != mosq_ms_queued){ |
22 |
case mosq_ms_wait_for_puback: |
23 |
new_state
= mosq_ms_publish_qos1; |
25 |
case mosq_ms_wait_for_pubrec: |
26 |
new_state
= mosq_ms_publish_qos2; |
28 |
case mosq_ms_wait_for_pubrel: |
29 |
new_state
= mosq_ms_send_pubrec; |
31 |
case mosq_ms_wait_for_pubcomp: |
32 |
new_state
= mosq_ms_resend_pubrel; |
37 |
if (new_state
!= mosq_ms_invalid){ |
38 |
msg->timestamp
= mosquitto_time(); |
超時提前檢測完成後就可以進入poll等待了。等待完成後,對於有可讀事件的連接,調用loop_handle_reads_writes進行事件讀寫處理,對於監聽端口的事件,使用mqtt3_socket_accept去接受新連接。
loop_handle_reads_writes新事件處理函數比較簡單,主體還是循環判斷可讀可寫事件,進行相應的處理。具體不多介紹了,需要關注的是由於是異步讀寫,所以需要記錄上次讀寫狀態,以便下次進入上下午繼續讀取數據。可寫事件由_mosquitto_packet_write完成,可讀事件由_mosquitto_packet_read完成。
新客戶端連接的事件則由qtt3_socket_accept完成,其會將新連接放在db->contexts[i]數組的某個空位置,每次都會遍歷尋找一個空的槽位放新連接。這裏有個小優化其實就是用hints的機制,記錄上次的查找位置,避免多次重複的從前面找到後面。
連接讀寫事件處理完成後,mosquitto會檢測是否需要重新reload部分配置文件。這個由SIGHUP的信號觸發。
限於篇幅,具體的邏輯請求處理下次再介紹了。
三、總結
mosquitto是一個簡單可依賴的開源MQTT實現,能支持10W左右的同時在線(未親測),單機版本,但通過bridge橋接模式支持部分分佈式,但有限;協議本身非常適合在移動設備上使用,耗電少,處理快,屬於header上帶有消息體長度的協議,這個在異步網絡事件代碼編寫時是碼農最愛的,哈哈。
對於後續的提高優化的地方,簡單記錄幾點:
- 發送數據用writev
- poll -> epoll ,用以支持更高的冰法;
- 改爲單線程版本,降低鎖開銷,目前鎖開銷還是非常大的。目測可以改爲單進程版本,類似redis,精心維護的話應該能達到不錯的效果;
- 網絡數據讀寫使用一次儘量多讀的方式,避免多次進入系統調用;
- 內存操作優化。不free,留着下次用;
- 考慮使用spwan-fcgi的形式或者內置一次啓動多個實例監聽同一個端口。這樣能更好的發揮機器性能,達到更高的性能;
初步接觸mosquitto && MQTT協議,弄錯的地方大家指正一下。