zeromq源代码分析6-3------ROUTER和DEALER

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;
}

1. 这个例子有zmq_poll的使用,这个后面章节我们会讲,现在你可以简单理解和多路复用器select, poll等类似的功能,不过还能够从相应的管道中轮询事件。

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

这边其实就是从管道数组里轮流选择一个active的管道,但要处理轮到的管道不active的情况。

我们先看一下管道数组的数据结构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_]);
        }


下面我们来看lb_t的实现:

总体来说的原理是,管道数组中会有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;
}

请看注释。


3. 因为设置了sink的对象为自身,那么当管道被激活的时候就会调用activated(1)函数:
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]







你可能感兴趣的:(网络编程)