標籤:backend 速度慢 api eal 經典的 標記 順序 通訊協定 reply
訊息佇列庫——ZeroMQ
ZeroMQ(簡稱ZMQ)是一個基於訊息佇列的多線程網路程式庫,其對通訊端類型、串連處理、幀、甚至路由的底層細節進行抽象,提供跨越多種傳輸協議的通訊端。
ZMQ是網路通訊中新的一層,介於應用程式層和傳輸層之間(按照TCP/IP劃分),其是一個可伸縮層,可並行運行,分散在分布式系統間。
ZMQ不是單獨的服務,而是一個嵌入式庫,它封裝了網路通訊、訊息佇列、線程調度等功能,向上層提供簡潔的API,應用程式通過載入庫檔案,調用API函數來實現高效能網路通訊。
主線程與I/O線程:
I/O線程,ZMQ根據使用者調用zmq_init函數時傳入的參數,建立對應數量的I/O線程。每個I/O線程都有與之綁定的Poller,Poller採用經典的Reactor模式實現。
Poller根據不同作業系統平台使用不同的網路I/O模型(select、poll、epoll、devpoll、kequeue等),所有的I/O操作都是非同步,線程不會被阻塞。。
主線程與I/O線程通過Mail Box傳遞訊息來進行通訊。
Server,在主線程建立zmq_listener,通過Mail Box發訊息的形式將其綁定到I/O線程,I/O線程把zmq_listener添加到Poller中用以偵聽讀事件。
Client,在主線程中建立zmq_connecter,通過Mail Box發訊息的形式將其綁定到I/O線程,I/O線程把zmq_connecter添加到Poller中用以偵聽寫事件。
Client與Server第一次通訊時,會建立zmq_init來發送identity,用以進行認證。認證結束後,雙方會為此次串連建立Session,以後雙方就通過Session進行通訊。
每個Session都會關聯到相應的讀/寫管道, 主線程收發訊息只是分別從管道中讀/寫資料。Session並不實際跟kernel交換I/O資料,而是通過plugin到Session中的Engine來與kernel交換I/O資料。
ZMQ將訊息通訊分成4種模型:
- 一對一結對模型(Exclusive-Pair),可以認為是一個TCP Connection,但是TCP Server只能接受一個串連。資料可以雙向流動,這點不同於後面的請求回應模型。
- 請求回應模型(Request-Reply),由Client發起請求,並由Server響應,跟一對一結對模型的區別在於可以有多個Client。
- 發布訂閱模型(Publish-Subscribe),Publish端單向分發資料,且不關心是否把全部資訊發送給Subscribe端。如果Publish端開始發布資訊時,Subscribe端尚未串連進來,則這些資訊會被直接丟棄。Subscribe端只能接收,不能反饋,且在Subscribe端消費速度慢於Publish端的情況下,會在Subscribe端堆積資料。
- 管道模型(Push-Pull),從 PUSH 端單向的向 PULL 端單向的推送資料流。如果有多個PULL端同時串連到PUSH端,則PUSH端會在內部做一個負載平衡,採用平均分配的演算法,將所有訊息均衡發布到PULL端上。與發布訂閱模型相比,管道模型在沒有消費者的情況下,發布的訊息不會被消耗掉;在消費者能力不夠的情況下,能夠提供多消費者並行消費解決方案。該模型主要用於多任務並行。
這4種模型總結出了通用的網路通訊模型,在實際中可以根據應用需要,組合其中的2種或多種模型來形成自己的解決方案。
ZMQ提供進程內(inproc://)、進程間(ipc://)、機器間(tcp://)、廣播(pgm://)等四種通訊協定。
ZMQ API
ZMQ提供的所有API均以zmq_開頭,
#include <zmq.h>gcc [flags] files -lzmq [libraries]
例如,返回當前ZMQ庫的版本資訊
void zmq_version (int *major, int *minor, int *patch);
Context
在使用任何ZQM庫函數之前,必須首先建立ZMQ context(上下文),程式終止時,也需要銷毀context。
建立context
void *zmq_ctx_new ();
ZMQ context是安全執行緒的,可以在多線程環境使用,而不需要程式員對其加/解鎖。
在一個進程中,可以有多個ZMQ context並存。
設定context選項
int zmq_ctx_set (void *context, int option_name, int option_value);int zmq_ctx_get (void *context, int option_name);
銷毀context
int zmq_ctx_term (void *context);
Sockets
ZMQ Sockets 是代表非同步訊息佇列的一個抽象,注意,這裡的ZMQ socket和POSIX通訊端的socket不是一回事,ZMQ封裝了物理串連的底層細節,對使用者不透明。
傳統的POSIX通訊端只能支援1對1的串連,而ZMQ socket支援多個Client的並發串連,甚至在沒有任何對端(peer)的情況下,ZMQ sockets上也能放入訊息;
ZMQ sockets不是安全執行緒的,因此,不要在多個線程中並行操作同一個sockets。
建立ZMQ Sockets
void *zmq_socket (void *context, int type);
注意,ZMQ socket在bind之前還不能使用。
type參數含義
|
pattern |
type |
description |
|
一對一結對模型 |
ZMQ_PAIR |
|
|
請求回應模型 |
ZMQ_REQ |
client端使用 |
|
ZMQ_REP |
server端使用 |
|
ZMQ_DEALER |
將訊息以輪詢的方式分發給所有對端(peers) |
|
ZMQ_ROUTER |
|
|
發布訂閱模型 |
ZMQ_PUB |
publisher端使用 |
|
ZMQ_XPUB |
|
|
ZMQ_SUB |
subscriber端使用 |
|
ZMQ_XSUB |
|
|
管道模型 |
ZMQ_PUSH |
push端使用 |
|
ZMQ_PULL |
pull端使用 |
|
原生模型 |
ZMQ_STREAM |
|
設定socket選項
int zmq_getsockopt (void *socket, int option_name, void *option_value, size_t *option_len);int zmq_setsockopt (void *socket, int option_name, const void *option_value, size_t option_len);
關閉socket
int zmq_close (void *socket);
建立一個訊息流程
int zmq_bind (void *socket, const char *endpoint);int zmq_connect (void *socket, const char *endpoint);
bind函數是將socket綁定到本地的端點(endpoint),而connect函數串連到指定的peer端點。
endpoint支援的類型:
|
transports |
description |
uri example |
|
zmp_tcp |
TCP的單播通訊 |
tcp://*:8080 |
|
zmp_ipc |
本地處理序間通訊 |
ipc:// |
|
zmp_inproc |
本地線程間通訊 |
inproc:// |
|
zmp_pgm |
PGM廣播通訊 |
pgm:// |
收發訊息
int zmq_send (void *socket, void *buf, size_t len, int flags);int zmq_recv (void *socket, void *buf, size_t len, int flags);int zmq_send_const (void *socket, void *buf, size_t len, int flags);
zmq_recv()函數的len參數指定接收buf的最大長度,超出部分會被截斷,函數返回的值是接收到的位元組數,返回-1表示出錯;
zmq_send()函數將指定buf的指定長度len的位元組寫入隊列,函數傳回值是發送的位元組數,返回-1表示出錯;
zmq_send_const()函數表示發送的buf是一個常量記憶體區(constant-memory),這塊記憶體不需要複製、釋放。
socket事件監控
int zmq_socket_monitor (void *socket, char * *addr, int events);
zmq_socket_monitor()函數會產生一對sockets,publishers端通過inproc://協議發布 sockets狀態改變的events;
訊息包含2幀,第1幀包含events id和關聯值,第2幀表示受影響的endpoint。
監控支援的events:
ZMQ_EVENT_CONNECTED: 建立串連
ZMQ_EVENT_CONNECT_DELAYED: 串連失敗
ZMQ_EVENT_CONNECT_RETRIED: 非同步串連/重連
ZMQ_EVENT_LISTENING: bind到端點
ZMQ_EVENT_BIND_FAILED: bind失敗
ZMQ_EVENT_ACCEPTED: 接收請求
ZMQ_EVENT_ACCEPT_FAILED: 接收請求失敗
ZMQ_EVENT_CLOSED: 關閉串連
ZMQ_EVENT_CLOSE_FAILED: 關閉串連失敗
ZMQ_EVENT_DISCONNECTED: 會話(tcp/ipc)中斷
I/O多工
int zmq_poll (zmq_pollitem_t *items, int nitems, long timeout);
對sockets集合的I/O多工,使用水平觸發。
與epoll類似,items參數指定一個結構體數組(結構體定義如下),nitems指定數組的元素個數,timeout參數是逾時時間(單位:ms,0表示不等待立即返回,-1表示阻塞等待)。
typedef struct{ void *socket; int fd; short events; short revents;} zmq_pollitem_t;
對於每個zmq_pollitem_t元素,ZMQ會同時檢查其socket(ZMQ通訊端)和fd(原生通訊端)上是否有指定的events發生,且ZMQ通訊端優先。
events指定該sockets需要關注的事件,revents返回該sockets已發生的事件,它們的取值為:
- ZMQ_POLLIN,可讀;
- ZMQ_POLLOUT,可寫;
- ZMQ_POLLERR,出錯;
Messages
一個ZMQ訊息就是一個用於在訊息佇列(進程內部或跨進程)中進行傳輸的資料單元,ZMQ訊息本身沒有資料結構,因此支援任意類型的資料,這完全依賴於程式員如何定義訊息的資料結構。
一條ZMQ訊息可以包含多個訊息片(multi-part messages),每個訊息片都是一個獨立zmq_msg_t結構。
ZMQ保證以原子方式傳遞訊息,要麼所有訊息片都發送成功,要麼都不成功。
初始化訊息
typedef void (zmq_free_fn) (void *data, void *hint);int zmq_msg_init (zmq_msg_t *msg);int zmq_msg_init_data (zmq_msg_t *msg, void *data, size_t size, zmq_free_fn *ffn, void *hint);int zmq_msg_init_size (zmq_msg_t *msg, size_t size);
zmq_msg_init()函數初始化一個訊息對象zmq_msg_t ,不要直接存取zmq_msg_t對象,可以通過zmq_msg_* 函數來訪問它。
zmq_msg_init()、zmq_msg_init_data()、zmq_msg_init_size() 三個函數是互斥的,每次使用其中一個即可。
設定訊息屬性
int zmq_msg_get (zmq_msg_t *message, int property);int zmq_msg_set (zmq_msg_t *message, int property, int value);
釋放訊息
int zmq_msg_close (zmq_msg_t *msg);
收發訊息
int zmq_msg_send (zmq_msg_t *msg, void *socket, int flags);int zmq_msg_recv (zmq_msg_t *msg, void *socket, int flags);
其中,flags參數如下:
ZMQ_DONTWAIT,非阻塞模式,如果沒有可用的訊息,將errno設定為EAGAIN;
ZMQ_SNDMORE,發送multi-part messages時,除了最後一個訊息片外,其它每個訊息片都必須使用 ZMQ_SNDMORE 標記位。
擷取訊息內容
void *zmq_msg_data (zmq_msg_t *msg);int zmq_msg_more (zmq_msg_t *message);size_t zmq_msg_size (zmq_msg_t *msg);
zmq_msg_data()返回指向訊息對象所帶內容的指標;
zmq_msg_size()返回訊息的位元組數;
zmq_msg_more()標識該訊息片是否是整個訊息的一部分,是否還有更多的訊息片待接收;
控制訊息
int zmq_msg_copy (zmq_msg_t *dest, zmq_msg_t *src);int zmq_msg_move (zmq_msg_t *dest, zmq_msg_t *src);
zmq_msg_copy()函數實現的是淺拷貝;
zmq_msg_move()函數中,將dst指向src訊息,然後src被置空。
eg,接收訊息的程式碼範例:
zmq_msg_t part;while (true) { // Create an empty ?MQ message to hold the message part int rc = zmq_msg_init (&part); assert (rc == 0); // Block until a message is available to be received from socket rc = zmq_msg_recv (socket, &part, 0); assert (rc != -1); if (zmq_msg_more (&part)) fprintf (stderr, "more\n"); else { fprintf (stderr, "end\n"); break; } zmq_msg_close (&part); }
代理
ZMQ提供代理功能,代理可以在前端socket和後端socket之間轉寄訊息。
int zmq_proxy (const void *frontend, const void *backend, const void *capture);int zmq_proxy_steerable (const void *frontend, const void *backend, const void *capture, const void *control);
共用隊列(shared queue),前端是ZMQ_ROUTER socket,後端是ZMQ_DEALER socket,proxy會把clients發來的請求,公平地分發給services;
轉寄隊列(forwarded),前端是ZMQ_XSUB socket, 後端是ZMQ_XPUB socket, proxy會把從publishers收到的訊息轉寄給所有的subscribers;
流(streamer),前端是ZMQ_PULL socket, 後端是ZMQ_PUSH socket.
proxy使用的一個樣本:
// Create frontend and backend socketsvoid *frontend = zmq_socket (context, ZMQ_ROUTER);assert (backend);void *backend = zmq_socket (context, ZMQ_DEALER);assert (frontend);// Bind both sockets to TCP portsassert (zmq_bind (frontend, "tcp://*:5555") == 0);assert (zmq_bind (backend, "tcp://*:5556") == 0);// Start the queue proxy, which runs until ETERM zmq_proxy frontend, backend, NULL);
錯誤處理
ZMQ庫使用POSIX處理函數錯誤,返回NULL指標或者負數時表示調用出錯。
int zmq_errno (void);const char *zmq_strerror (int errnum);
zmq_errno()函數返回當前線程的錯誤碼errno變數的值;
zmq_strerror()函數將錯誤映射成錯誤字串。
加密傳輸
ZQM可以為IPC和TCP串連提供安全機制:
這些通過 zmq_setsockopt()函數設定socket選項的時候配置。
總結:
1、僅僅提供24個API介面,風格類似於BSD Socket。
2、處理了網路異常,包括串連異常中斷、重連等。
3、改變TCP基於位元組流收發資料的方式,處理了粘包、半包等問題,以msg為單位收發資料,結合Protocol Buffers,可以對應用程式層徹底屏蔽網路通訊層。
4、對大資料通過SENDMORE/RECVMORE提供分包收發機制。
5、通過線程間資料流動來保證同一時刻任何資料都只會被一個線程持有,以此實現多線程的“去鎖化”。
6、通過高水位HWM來控制流程量,用交換SWAP來轉儲記憶體資料,彌補HWM遺失資料的缺陷。
7、伺服器端和用戶端的啟動沒有先後順序。
訊息佇列庫——ZeroMQ