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 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
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]