zeromq浅析

zeromq对传统的⽹络接⼝进⾏再次封装,提供了⼀个跨语⾔的库,⽀持任意语⾔使⽤统⼀的接⼝。ZMQ不是单独的服务,⽽是⼀个嵌⼊式库,它封装了⽹络通信、消息队列、线程调度等功能,向上层提供简洁的API,应⽤程序通过加载库⽂件,调⽤API函数来实现⾼性能⽹络通信。简单来说,zeromq就是实现了消息队列的一个库。这里只简单介绍一下zeromq线程间的交互与四个模型的实现。

主线程与IO线程

ZMQ根据⽤户调⽤zmq_init函数时传⼊的参数,创建对应数量的I/O线程。每个I/O线程都有与之绑定的Poller,Poller采⽤经典的Reactor模式实现,处理具体的网络IO操作。主线程处理具体的业务,比如sub、pub、push、pull。
在这里插入图片描述
可以看出,主线程与IO线程之间的交互是通过管道实现的。注意这里的管道使用的是无锁队列。由于无锁队列不能阻塞,当管道中没有数据时,就需要通过命令去阻塞,当有数据到来时,通过mailbox去发命令通知主线程,然后调用recv()。这里特别提醒一下,不需要每次来数据都通知,只是在管道中数据从无到有时才通知,从有到无会阻塞。这有点像epoll的边沿触发。

消息协议设计

01 00  # 消息分隔符
01 05 48 65 6c 6c 6f   # 第一个字节表示是否还有后续数据,即ZMQ_RCVMORE,第二个字节代表数据长度,这里是5字节,后面是数据,即“Hello”
00 07 20 64 61 72 72 65 6e   # 第一个字节为0,则没有后续数据,即本帧为最后一帧。后面7字节为内容

常用模型

这里介绍zeromq用到的四种模型。

请求响应模型

ZMQ_REQ请求端

请求端主要有以下几个函数。
发送数据

int zmq::req_t::xsend (msg_t *msg_);

这是发送数据的接口,后面讲的其他模型也是xsend(),只不过是在不同的类中。
通过_receiving_reply控制send的发送,当_receiving_reply为true时等待对⽅应答。当消息分帧发送时,只有最后⼀帧才将_receiving_reply设置为true,可以对应协议来理解。

//  If we've sent a request and we still haven't got the reply,
//  we can't send another request unless the strict option is disabled.
if (_receiving_reply) {
    if (_strict) {
        errno = EFSM;
        return -1;
    }

    _receiving_reply = false;
    _message_begins = true;
}

接收数据

int zmq::req_t::xrecv (msg_t *msg_);

接收完同⼀个消息的所有帧才将_receiving_reply置为false,运⾏继续send。

//  If the reply is fully received, flip the FSM into request-sending state.
if (!(msg_->flags () & msg_t::more)) {
    _receiving_reply = false;
    _message_begins = true;
}

ZMQ_REP响应端

响应端的函数类似。函数中的源码就不贴了,只给出一些说明。
发送数据

int zmq::rep_t::xsend (msg_t *msg_);

通过_sending_reply控制send的发送,当_sending_reply为false时需要对⽅发请求后才能send数据;当消息分帧时,只有最后⼀帧才将_sending_reply设置为false,说明send rep完成,等待新的req。
接收数据

int zmq::rep_t::xrecv (msg_t *msg_)

_sending_reply如果为true,说明还没有回复消息,此时不能再接收数据。接收完同⼀个消息的所有帧才将_sending_reply置为true,说明转到rep状态,只有send rep后才能再接收数据。

发布订阅模型

SUB订阅者要先到PUB发布端订阅数据。
模式的核⼼在于:

  1. subscriber向publisher注册filter (topic),注意不同的subscriber注册的topic可以相同
  2. publisher根据filter(topic)向对应的subscriber发布消息
  3. 每个subscriber订阅者都有自己的一套管道

zeromq使⽤了两组socket_base_t的派⽣类。
xsub_t和sub_t⽤于sub端。sub_t的xsetsockopt()⽤于设置filter选项,它会调⽤xsub_t的xsend()向pub端发送⼀个注册请求。当sub端收到发布的消息时,会暂存在xsub_t的fq_t成员中,以备以后⽤户调⽤xrecv()来获取。

int zmq::sub_t::xsetsockopt (int option_,
                             const void *optval_,
                             size_t optvallen_)
{
    if (option_ != ZMQ_SUBSCRIBE && option_ != ZMQ_UNSUBSCRIBE) {
        errno = EINVAL;
        return -1;
    }
    //  Create the subscription message.
    msg_t msg;
    int rc;
    const unsigned char *data = static_cast<const unsigned char *> (optval_);
    if (option_ == ZMQ_SUBSCRIBE) {
        rc = msg.init_subscribe (optvallen_, data);
    } else {
        rc = msg.init_cancel (optvallen_, data);
    }
    errno_assert (rc == 0);

    //  Pass it further on in the stack.
    rc = xsub_t::xsend (&msg);
    return close_and_return (&msg, rc);
}

xpub_t和pub_t⽤于pub端。 mtrie_t成员和dist_t成员配合使⽤,⽤于过滤和发布消息。mtrie_t是⼀种字典树,⽀持prefix match模式。sub端注册filter时,对应的pipe_t同时保存在mtrie_t和dist_t中,在mtrie_t中根据消息头(可以理解为topic)找到期望的⽬标pipe_t时,会调⽤mark_as_matching()在dist_t中标记pipe_t,然后dist_t根据标记发送消息。

void generic_mtrie_t<T>::match (prefix_t data_, size_t size_,
                                void (*func_) (value_t *pipe_, Arg arg_),  //这里的回调函数会传入mark_as_matching
                                Arg arg_)
{
    for (generic_mtrie_t *current = this; current; data_++, size_--) {
    // Signal the pipes attached to this node.
    if (current->_pipes) {
        for (typename pipes_t::iterator it = current->_pipes->begin (),end = current->_pipes->end ();it != end; ++it) {
            func_ (*it, arg_);  //匹配时会调用
        }
    }
    // If we are at the end of the message, there's nothing more to match.
    if (!size_)
        break;
    // If there are no subnodes in the trie, return.
    if (current->_count == 0)
        break;
    if (current->_count == 1) {
        // If there's one subnode (optimisation).
        if (data_[0] != current->_min) {
            break;
        }
        current = current->_next.node;
        } else {
            // If there are multiple subnodes.
            if (data_[0] < current->_min || data_[0] >= current->_min + current->_count) {
               break;
        }
        current = current->_next.table[data_[0] - current->_min];
        }
    }
}

推拉模式

每个收到的消息都有⼀个routing_id,它是⼀个32位⽆符号整数。要将消息发送到给定的CLIENT对等⽅,应⽤程序必须在消息上设置对等⽅的 routing_id。如果未指定routing_id,或者未引⽤已连接的客户端对等⽅,则发送调⽤将失败。如果客户端对等⽅的传出缓冲区已满,则发送调⽤将阻塞。在任何情况下,SERVER套接字都不会丢弃消息。
push端和pull端都可以作为服务端或客户端,但是一个端不能同时是客户端和服务端。
pull端读取各个push端的数据,使用的是公平队列。

int zmq::pull_t::xrecv (msg_t *msg_)
{
    return _fq.recv (msg_);
}

注意pull_t中没有xsend(),同样push_t没有xrecv()。
push端给各个pull端发送数据,是使用的负载均衡。

int zmq::push_t::xsend (msg_t *msg_)
{
    return _lb.send (msg_);
}

这里的公平队列和负载均衡,本质上都是轮询。

int zmq::lb_t::sendpipe (msg_t *msg_, pipe_t **pipe_){
// Drop the message if required. If we are at the end of the message
...
    while (_active > 0) {
        if (_pipes[_current]->write (msg_)) { // 发送数据到pipe
        if (pipe_)
            *pipe_ = _pipes[_current];
            break;
    }
    // If send fails for multi-part msg rollback other
        // parts sent earlier and return EAGAIN.
        // Application should handle this as suitable
        if (_more) {
            _pipes[_current]->rollback ();
            _dropping = (msg_->flags () & msg_t::more) != 0;
            _more = false;
            errno = EAGAIN;
            return -2;
    }
        _active--;
        if (_current < _active)
            _pipes.swap (_current, _active);
        else
            _current = 0;
}
...
    return 0;
}

这是负载均衡的代码,可以看出是轮询,公平队列代码就不贴了。

ZMQ_DEALER和ZMQ_ROUTER路由模型

本质上是多个req端对应多个rep端。这里也用到了刚才提到的公平队列和负载均衡。
在这里插入图片描述
这里需要了解的一个问题是,rep端回发时如何找到发送请求的req端?答案是routing_id。
发送时,req端会在router绑定一个routing_id,在dealer发送给rep端时,会附带这个routing_id。回发时,router会根据这个id找到对应的pipe,再往这个pipe中写数据,回发时,也会附带routing_id,但是router回发给req时不带routing_id(其实req给router发送数据时也没有routing_id,这个id的绑定和存储都是在router内部的)。

# REQ->ZMQ_ROUTER
01 00
00 05 48 65 6c 6c 6f  # "Hello"

# ZMQ_ROUTER->REQ
01 00
00 05 57 6f 72 6c 64  # "World"

# ZMQ_DEALER->REP
01 05 00 6b 8b 45 68  # routing_id
01 00
00 05 48 65 6c 6c 6f  # "Hello"

# REP->ZMQ_DEALER
01 05 00 6b 8b 45 68  # routing_id
01 00
00 05 57 6f 72 6c 64  # "World"

当客户端断开重连时,routing_id会改变,会如同连接一个新的客户端一样在已有值上+1。
此外,router路由类是rep的基类,带有响应的一些功能;dealer是req的基类,带有发送的一些功能。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值