本文主要講zeromq的訊息體系zmq_msg_t及其相關的操作函數。
因為tcp是一種位元組流類型的協議,木有邊界,所以把該訊息邊界的制定留給了應用程式層。通常有兩種方式實現:
1. 在傳輸的資料中添加分隔字元。
2. 在每條訊息中添加size欄位。
而zeromq可以說選擇了第二種方式。
先來看看zmq_msg_t的基本資料結構
/* A message. Note that 'content' is not a pointer to the raw data. *//* Rather it is pointer to zmq::msg_content_t structure *//* (see src/msg_content.hpp for its definition). */typedef struct{ void *content; unsigned char flags; unsigned char vsm_size;// 小訊息的大小 unsigned char vsm_data [ZMQ_MAX_VSM_SIZE];// 小訊息的記憶體地區} zmq_msg_t其中content指標指向的是zmq::msg_content_t的結構, flags表示訊息的一些flags
這邊vsm_size和vsm_data是針對very small message的小訊息做一些最佳化,直接在stack上分配記憶體了,可以看後面的訊息函數的具體操作。
// Shared message buffer. Message data are either allocated in one // continuous block along with this structure - thus avoiding one // malloc/free pair or they are stored in used-supplied memory. // In the latter case, ffn member stores pointer to the function to be // used to deallocate the data. If the buffer is actually shared (there // are at least 2 references to it) refcount member contains number of // references. struct msg_content_t { void *data; size_t size; zmq_free_fn *ffn; void *hint; zmq::atomic_counter_t refcnt; };
其中data指向真正的訊息資料,size表示訊息資料的位元組大小,zmq_free_fn *ffn指向釋放函數,refcnt表示訊息的引用計數,hint目前未知。
下面我們看一下基本的訊息作業函數:
int zmq_msg_init_size (zmq_msg_t *msg_, size_t size_){ if (size_ <= ZMQ_MAX_VSM_SIZE) { msg_->content = (zmq::msg_content_t*) ZMQ_VSM; msg_->flags = (unsigned char) ~ZMQ_MSG_MASK; msg_->vsm_size = (uint8_t) size_; } else { msg_->content = (zmq::msg_content_t*) malloc (sizeof (zmq::msg_content_t) + size_); if (!msg_->content) { errno = ENOMEM; return -1; } msg_->flags = (unsigned char) ~ZMQ_MSG_MASK; zmq::msg_content_t *content = (zmq::msg_content_t*) msg_->content; content->data = (void*) (content + 1); content->size = size_; content->ffn = NULL; content->hint = NULL; new (&content->refcnt) zmq::atomic_counter_t (); // 設定引用計數 } return 0;}
對於小訊息來說, 我們設定content指向地址ZMQ_VSM,這個值為32,相當於一個魔法數字,用來表示小訊息。
對於相對來說的大訊息來說,我們malloc (sizeof (zmq::msg_content_t) + size_)的空間,其中size_就是可變長的訊息大小,然後設定content->data = (void *) (content + 1),指向這塊可變長的記憶體地區。
這邊有一個初始化msg的flags的操作:
msg_->flags = (unsigned char) ~ZMQ_MSG_MASK;
flags的定義:
/* Message flags. ZMQ_MSG_SHARED is strictly speaking not a message flag *//* (it has no equivalent in the wire format), however, making it a flag *//* allows us to pack the stucture tigher and thus improve performance. */#define ZMQ_MSG_MORE 1 // 00000001#define ZMQ_MSG_SHARED 128 // 10000000#define ZMQ_MSG_MASK 129 /* Merges all the flags */ // 10000001
我們簡單分析下這樣就可以msg的flags的初始值就是01111110,即可以| ZMQ_MSG_MORE或者| ZMQ_MSG_SHARED。
尼瑪。。。感覺在說廢話。。。
我們再來看看另外兩個訊息初始化函數:
int zmq_msg_init (zmq_msg_t *msg_){ msg_->content = (zmq::msg_content_t*) ZMQ_VSM; msg_->flags = (unsigned char) ~ZMQ_MSG_MASK; msg_->vsm_size = 0; return 0;}初始化一個msg訊息,設定成小訊息類型,初始化flags,並且將小訊息的大小初始化為0.
int zmq_msg_init_data (zmq_msg_t *msg_, void *data_, size_t size_, zmq_free_fn *ffn_, void *hint_){ msg_->content = (zmq::msg_content_t*) malloc (sizeof (zmq::msg_content_t)); alloc_assert (msg_->content); msg_->flags = (unsigned char) ~ZMQ_MSG_MASK; zmq::msg_content_t *content = (zmq::msg_content_t*) msg_->content; content->data = data_; content->size = size_; content->ffn = ffn_; content->hint = hint_; new (&content->refcnt) zmq::atomic_counter_t (); return 0;}
該函數根據已有的data和相應的size,和銷毀的函數,以及hint來初始化訊息。
這邊訊息指向data指標所指的記憶體地區。
接下來我們來看看擷取資料的地址的函數,這樣你就能操作訊息的資料記憶體地區了。
void *zmq_msg_data (zmq_msg_t *msg_){ zmq_assert ((msg_->flags | ZMQ_MSG_MASK) == 0xff); if (msg_->content == (zmq::msg_content_t*) ZMQ_VSM) return msg_->vsm_data; if (msg_->content == (zmq::msg_content_t*) ZMQ_DELIMITER) return NULL; return ((zmq::msg_content_t*) msg_->content)->data;}如果是小訊息那麼就是stack上的vsm_data地址,大訊息的就是heap上的content->data。
如果是delimiter的話,返回的是null 指標。 我們後面會講述delimiter的用處。
而zmq_msg_size(1)函數也是類似的處理方式,只不過返回的是訊息大小。
size_t zmq_msg_size (zmq_msg_t *msg_){ zmq_assert ((msg_->flags | ZMQ_MSG_MASK) == 0xff); if (msg_->content == (zmq::msg_content_t*) ZMQ_VSM) return msg_->vsm_size; if (msg_->content == (zmq::msg_content_t*) ZMQ_DELIMITER) return 0; return ((zmq::msg_content_t*) msg_->content)->size;}
接下來我們來看zmq_msg_close(1)函數, 看看如何銷毀訊息的。
int zmq_msg_close (zmq_msg_t *msg_){ // Check the validity tag. if (unlikely (msg_->flags | ZMQ_MSG_MASK) != 0xff) { errno = EFAULT; return -1; } // For VSMs and delimiters there are no resources to free. if (msg_->content != (zmq::msg_content_t*) ZMQ_DELIMITER && msg_->content != (zmq::msg_content_t*) ZMQ_VSM) { // If the content is not shared, or if it is shared and the reference. // count has dropped to zero, deallocate it. zmq::msg_content_t *content = (zmq::msg_content_t*) msg_->content; if (!(msg_->flags & ZMQ_MSG_SHARED) || !content->refcnt.sub (1)) { // We used "placement new" operator to initialize the reference. // counter so we call its destructor now. content->refcnt.~atomic_counter_t (); if (content->ffn) content->ffn (content->data, content->hint); free (content); } } // Remove the validity tag from the message. msg_->flags = 0; return 0;}
訊息的flag檢測和前面類似,該函數末尾除還會移除初始化的flags,將其置成0,表明該訊息已經廢了。
對於小訊息,訊息資料是分配在stack上的,因此不需要手動銷毀。
對於大訊息,訊息資料是分配在heap上的,因此我們查看訊息flags是否採用ZMQ_MSG_SHARED共用模式,如果沒有使用就直接銷毀。假如使用了共用模式,那麼我們遞減訊息的引用計數,一旦引用計數為0,我們就銷毀訊息內容。
銷毀過程如下:
1. 銷毀引用計數,因為在建立引用計數的時候我們使用的是placement new所以這邊我們需要調用它的解構函式。
2. 如果有註冊相應的自訂的銷毀函數,就調用該函數。這種情況主要是用於使用zmq_msg_init_data(),自己來管理data所指向空間。
3. 調用free(1)釋放heap的空間, 注意 如果是使用zmq_msg_init_size()來初始化訊息的,就會釋放掉data的空間,因為這塊空間就是分配時多分配的size大小(在content下方)的那塊。
至於zmq_msg_move和zmq_msg_copy主要是訊息的move和copy。
int zmq_msg_move (zmq_msg_t *dest_, zmq_msg_t *src_){#if 0 // Check the validity tags. if (unlikely ((dest_->flags | ZMQ_MSG_MASK) != 0xff || (src_->flags | ZMQ_MSG_MASK) != 0xff)) { errno = EFAULT; return -1; }#endif zmq_msg_close (dest_); *dest_ = *src_; zmq_msg_init (src_); return 0;}int zmq_msg_copy (zmq_msg_t *dest_, zmq_msg_t *src_){ // Check the validity tags. if (unlikely ((dest_->flags | ZMQ_MSG_MASK) != 0xff || (src_->flags | ZMQ_MSG_MASK) != 0xff)) { errno = EFAULT; return -1; } zmq_msg_close (dest_); // VSMs and delimiters require no special handling. if (src_->content != (zmq::msg_content_t*) ZMQ_DELIMITER && src_->content != (zmq::msg_content_t*) ZMQ_VSM) { // One reference is added to shared messages. Non-shared messages // are turned into shared messages and reference count is set to 2. zmq::msg_content_t *content = (zmq::msg_content_t*) src_->content; if (src_->flags & ZMQ_MSG_SHARED) content->refcnt.add (1); else { src_->flags |= ZMQ_MSG_SHARED; content->refcnt.set (2); } } *dest_ = *src_; return 0;}
zmq_msg_move(1)將source的訊息重設成初始化的空訊息。
而zmq_msg_copy(1)dst和src共用訊息content的引用,主要注意引用計數的變更。如果已經是SHARED模式則增加引用計數,否則則設定成SHARED模式,並將引用技術設定成2。
有了這些知識,下面我們來看一下基本的訊息作業的樣本:
// Receive 0MQ string from socket and convert into C stringstatic char *s_recv (void *socket) { zmq_msg_t message; // 建立訊息結構 zmq_msg_init (&message); // 初始化空訊息 zmq_recv (socket, &message, 0); // 接收訊息 int size = zmq_msg_size (&message); // 計算訊息的大小 char *string = malloc (size + 1); // 分配string為指向size + 1大小的heap空間,那個多出來的1位元組是'\0'的空間 memcpy (string, zmq_msg_data (&message), size); // 通過zmq_msg_data(1)獲得訊息的data地址,拷貝到字串中 zmq_msg_close (&message); // 釋放或銷毀訊息 string [size] = 0; // 設定'\0' return (string);}// Convert C string to 0MQ string and send to socketstatic ints_send (void *socket, char *string) { int rc; zmq_msg_t message; // 建立訊息結構 zmq_msg_init_size (&message, strlen (string)); // 以字串長度(不包括'\0')初始化成訊息 memcpy (zmq_msg_data (&message), string, strlen (string)); // 將字串的內容(不包括'\0')拷貝給訊息 rc = zmq_send (socket, &message, 0); // 發送訊息 assert (!rc); zmq_msg_close (&message); // 釋放和銷毀訊息 return (rc);}
從上面的注釋可以看出收發訊息的流程。zeromq有相關的庫(czmq)封裝了這些操作,當然你也可以自己封裝。
ZMQ_DELIMITER:
剛才上文曾經提到過delimiter類型的訊息。這種類型的訊息類似於終結者的意思,主要在收發的管道中使用。因為zeromq會將訊息先發送到管道中,然後poller運行在另外一個線程,將管道中的資料讀出來發往socket的緩衝區,所以可以發送一個delimiter類型的訊息去終結管道,銷毀它。在以後我們分析管道的過程中,你能看到做這個工作的代碼。
總結:
本文簡單地介紹了zeromq中定義的訊息的資料結構以及相關的操作,基本上zeromq的訊息的資料結構就採用了可變長的資料結構來存放data,還會對小訊息進行記憶體配置上面的最佳化,直接使用stack分配,而不是使用heap動態分配,訊息content擁有引用計數可以共用之。
這邊我們看到訊息flags的時候我們發覺漏講了一個#define ZMQ_MSG_MORE 1的標誌,這個標誌是multipart message使用的。下一次我們就會分析這塊內容,並且會看看訊息發送到管道後,poller將其取出來發送給socket緩衝區的具體細節以及反過來接收訊息的過程,敬請期待。
希望有興趣的朋友可以和我聯絡,一起學習。 kaka11.chen@gmail.com