zeromq的zero表明它木有broker,但是并不代表着他不能有broker,而是作为device选项,使用ROUTER socket和DEALER socket就可以模拟一个简单broker。
可以从tutorial中看到模拟的代码:
// // Simple request-reply broker // #include "zhelpers.h" int main (void) { // Prepare our context and sockets void *context = zmq_init (1); void *frontend = zmq_socket (context, ZMQ_ROUTER); void *backend = zmq_socket (context, ZMQ_DEALER); zmq_bind (frontend, "tcp://*:5559"); zmq_bind (backend, "tcp://*:5560"); // Initialize poll set zmq_pollitem_t items [] = { { frontend, 0, ZMQ_POLLIN, 0 }, { backend, 0, ZMQ_POLLIN, 0 } }; // Switch messages between sockets while (1) { zmq_msg_t message; int64_t more; // Multipart detection zmq_poll (items, 2, -1); if (items [0].revents & ZMQ_POLLIN) { // router->dealer while (1) { // Process all parts of the message zmq_msg_init (&message); zmq_recv (frontend, &message, 0); size_t more_size = sizeof (more); zmq_getsockopt (frontend, ZMQ_RCVMORE, &more, &more_size); zmq_send (backend, &message, more? ZMQ_SNDMORE: 0); zmq_msg_close (&message); if (!more) break; // Last message part } } if (items [1].revents & ZMQ_POLLIN) { // dealer->router while (1) { // Process all parts of the message zmq_msg_init (&message); zmq_recv (backend, &message, 0); size_t more_size = sizeof (more); zmq_getsockopt (backend, ZMQ_RCVMORE, &more, &more_size); zmq_send (frontend, &message, more? ZMQ_SNDMORE: 0); zmq_msg_close (&message); if (!more) break; // Last message part } } } // We never get here but clean up anyhow zmq_close (frontend); zmq_close (backend); zmq_term (context); return 0; }
2. req发送消息给router,利用router接收消息,然后将相应的消息使用dealer发给rep。而反过来的过程也能够发送。
3. 对multipart message的处理。接收消息的时候会检测ZMQ_RCVMORE的属性,然后再发的时候会设置一样的flag。
其实ROUTER和DEALER就是上一篇blog我们曾经介绍过REQ和REP中使用过的XREQ和XREP。
1. 消息流向: REQ->ROUTER->DEALER->REP
a. REQ发送消息到ROUTER时会加上0字节的空消息部分。
b. ROUTER接收消息的时候会使用fair-queuing策略来选择一个REQ进行处理,通过加上REQ端的identity前缀部分作为返回地址来封装消息。
c. DEALER转发ROUTER收到的消息的时候会使用load-balance策略去选择一个REP来处理消息。
d. REP接受消息将DEALER的消息加上DEALER的identity前缀,通过该identity选择reply的管道,并且将消息empty部分以上保护empty部分的header全部写到reply管道中,但是identity不会写入。REP返回给上层的消息只是消息的数据部分,即empty部分下面的消息部分。
2. 消息流向: REP->DEALER->ROUTER->REQ
a. REP发送消息到DEALER的时候只需将reply的消息体写入reply的管道就行了,因为1-d部分REP接收消息的时候已经将header写入reply的管道了。
b. DEALER接收消息的时候会使用fair-queuing策略来选择处理REP。
c. ROUTER转发DEALER接收到的消息,而这时消息header中的前缀就是1-b加上的REQ端的identity,于是ROUTER会根据identity选择相应的管道发送消息给相应的REQ,但是注意identity部分不发送。
d. REQ接收到消息就把empty message part以下的消息,即reply data,返回给caller。
一下是消息的封包:
其中1-a部分将Frame 2加入,1-b部分将Frame1部分加入,而2-c部分讲Frame1部分移除。
由于上一篇blog我们已经给出了xreq,xrep的源代码,而根据这边的理解分析应该够了,我们就不详细讲了,但是我们会分析这里的load-balance和fair-queueing机制。
其实这两者的代码是类似的,都是轮流选择的方式,我们就看一下load-balance的代码,fair-queueing可以自己看下:
主要数据结构:
// List of outbound pipes. 管道数组 typedef array_t <class writer_t> pipes_t; pipes_t pipes; // Number of active pipes. All the active pipes are located at the // beginning of the pipes array. pipes_t::size_type active; // active的数目 // Points to the last pipe that the most recent message was sent to. pipes_t::size_type current; // 当前轮到的index
我们先看一下管道数组的数据结构array_t<T>:
1. 该数组的特点就是通过在item中保存相应的index来访问,插入和删除都是O(1)的复杂度。
2. 存放的是指针。
3. 所有需要被该数组存放的item必须是array_item_t class的子类。
该数组靠一个vector来保存item的指针。
1. push_back(1)的时候item设置index。
inline void push_back (T *item_) { if (item_) item_->set_array_index (items.size ()); items.push_back (item_); }
2. erase(1)的时候将最后一个元素的index变成要删除的index ,将删除元素位置的指针指向最后一个元素,并且将vector中最后一个指针pop_back()。
inline void erase (size_type index_) { if (items.back ()) items.back ()->set_array_index (index_); items [index_] = items.back (); items.pop_back (); }3. 因此这边用取index(1)函数需要访问item中保存的index。
inline size_type index (T *item_) { return (size_type) item_->get_array_index (); }4. 此外还有swap(2)等函数, 不但要交换指针,还要交换item的index。
inline void swap (size_type index1_, size_type index2_) { if (items [index1_]) items [index1_]->set_array_index (index2_); if (items [index2_]) items [index2_]->set_array_index (index1_); std::swap (items [index1_], items [index2_]); }
总体来说的原理是,管道数组中会有active的管道和inactive的管道。而active的管道就会在数组的前方,unactive的在后方。
1. 当管道attach的时候,会向pipes array中加入管道,交换到active中的最后一个,然后递增active的数目。
void zmq::lb_t::attach (writer_t *pipe_) { pipe_->set_event_sink (this); pipes.push_back (pipe_); pipes.swap (active, pipes.size () - 1); active++; if (terminating) { sink->register_term_acks (1); pipe_->terminate (); } }2. 而当发送的时候:
int zmq::lb_t::send (zmq_msg_t *msg_, int flags_) { // Drop the message if required. If we are at the end of the message // switch back to non-dropping mode. if (dropping) { more = msg_->flags & ZMQ_MSG_MORE; if (!more) dropping = false; int rc = zmq_msg_close (msg_); errno_assert (rc == 0); rc = zmq_msg_init (msg_); zmq_assert (rc == 0); return 0; } while (active > 0) { if (pipes [current]->write (msg_)) { // 写入成功就break more = msg_->flags & ZMQ_MSG_MORE; break; } // 失败的话继续 zmq_assert (!more); active--; if (current < active) pipes.swap (current, active); // 交换current和最后一个active的,于是最后一个现在成了inactive的首个,而current又变成了active继续尝试 else current = 0; // current前方有active管道。 } // If there are no pipes we cannot send the message. if (active == 0) { errno = EAGAIN; return -1; } // If it's final part of the message we can fluch it downstream and // continue round-robinning (load balance). if (!more) { // 等到整条消息完成后flush并且轮转到下一个active的。 pipes [current]->flush (); current = (current + 1) % active; } // Detach the message from the data buffer. int rc = zmq_msg_init (msg_); zmq_assert (rc == 0); return 0; }
请看注释。
void zmq::lb_t::activated (writer_t *pipe_) { // Move the pipe to the list of active pipes. pipes.swap (pipes.index (pipe_), active); active++; }通过交换放到管道前方的最后一个active中。
总结:
本文通过一个简单的REQ-ROUTER-DEALER-REP例子并结合源代码分析了zeromq中Request-Reply加入ROUTER-DEALER broker之后的相关流程。
下一篇我们会讲PUB和SUB。敬请期待!希望有兴趣的朋友可以和我联系,一起学习。 [email protected]