本文我们讲一下req和rep这对zeromq的socket。
这是一个经典的Request-Reply的例子。
代码也是很简单:
// // Hello World server // Binds REP socket to tcp://*:5555 // Expects "Hello" from client, replies with "World" // #include <zmq.h> #include <stdio.h> #include <unistd.h> #include <string.h> int main (void) { void *context = zmq_init (1); // 创建上下文, 初始化一个io_thread // Socket to talk to clients void *responder = zmq_socket (context, ZMQ_REP); // 创建REP类型的socket zmq_bind (responder, "tcp://*:5555"); // 绑定到端口并且在io_thread中accept连接 while (1) { // Wait for next request from client zmq_msg_t request; // 创建消息结构 zmq_msg_init (&request); // 初始化空的消息 zmq_recv (responder, &request, 0); // 从管道中接收消息 printf ("Received Hello\n"); zmq_msg_close (&request); // 销毁消息 // Do some 'work' sleep (1); // Send reply back to client zmq_msg_t reply; // 创建reply消息的结构 zmq_msg_init_size (&reply, 5); // 初始化5个字节的消息来容纳“World” memcpy (zmq_msg_data (&reply), "World", 5); // 拷贝到消息中 zmq_send (responder, &reply, 0); // 发送消息到管道,等待io_thread从管道中读取后发送 zmq_msg_close (&reply); } // We never get here but if we did, this would be how we end zmq_close (responder); zmq_term (context); return 0; }
// // Hello World client // Connects REQ socket to tcp://localhost:5555 // Sends "Hello" to server, expects "World" back // #include <zmq.h> #include <string.h> #include <stdio.h> #include <unistd.h> int main (void) { void *context = zmq_init (1); // 创建上下文, 初始化一个io_thread // Socket to talk to server printf ("Connecting to hello world server…\n"); void *requester = zmq_socket (context, ZMQ_REQ); zmq_connect (requester, "tcp://localhost:5555"); // 在io_thread中连接到端点 int request_nbr; for (request_nbr = 0; request_nbr != 10; request_nbr++) { // 发送10次 zmq_msg_t request; // 建立request的消息结构 zmq_msg_init_size (&request, 5); // 初始化request消息为5个字节 memcpy (zmq_msg_data (&request), "Hello", 5); // 设置request消息内容为"Hello" printf ("Sending Hello %d…\n", request_nbr); zmq_send (requester, &request, 0); // 发送消息到管道,等待io_thread从管道中读取后发送 zmq_msg_close (&request); // 销毁request消息 zmq_msg_t reply; // 建立reply的消息结构 zmq_msg_init (&reply); // 初始化reply消息为0字节的空消息 zmq_recv (requester, &reply, 0); // 从管道中接收消息 printf ("Received World %d\n", request_nbr); zmq_msg_close (&reply); // 销毁reply消息 } zmq_close (requester); zmq_term (context); return 0; }
从以前几篇blog的分析你应该明白以下几点:
1. 创建上下文的时候会创建指定数目的io_thread,这儿是1个。该io_thread采用reactor模式,会借助于poller不断地去轮询可读可写事件。
2. 最开始bind socket的时候会去选择一个io_thread来调用accept(),当有连接被accept后会互相交换对端的identity。
3. 交换完identity后,io_thread会建立一个session,并且attach到两个管道。socket调用send()和recv()的时候会和相应的管道交互。
4. io_thread当poller轮询到可读事件的时候,就会将数据写入到管道中或者有可写事件的时候,就会从管道中读数据。
ok,这些如果不明白的话,请回过头去结合源代码看前面的文章,如果还是不懂可以联系我。
接下去转入正题,我们来看今天的主角REQ和REP:
首先我们来介绍一下req和rep的消息封装的格式:
从前面的博文中我们已经了解了zeromq中multipart message,而zeromq中每一种socket在发送和接受的时候会封装相应的消息,在消息头部加上相应的消息头,这些消息头的消息的flag就是ZMQ_MSG_MORE,通过和后面的原始msg组成了一个multipart message。
以下就是REQ和REP的消息封装:
1. REQ发送消息:
原始消息:
REQ封装后的消息:
可以看到REQ发送的时候会加上一个0字节的message part。
我们看一下源代码:
int zmq::req_t::xsend (zmq_msg_t *msg_, int flags_) { // If we've sent a request and we still haven't got the reply, // we can't send another request. if (receiving_reply) { errno = EFSM; return -1; } // First part of the request is empty message part (stack bottom). if (message_begins) { // 加上empty message part的prefix zmq_msg_t prefix; int rc = zmq_msg_init (&prefix); zmq_assert (rc == 0); prefix.flags |= ZMQ_MSG_MORE; rc = xreq_t::xsend (&prefix, flags_); if (rc != 0) return rc; message_begins = false; } bool more = msg_->flags & ZMQ_MSG_MORE; int rc = xreq_t::xsend (msg_, flags_); // 发送消息 if (rc != 0) return rc; // If the request was fully sent, flip the FSM into reply-receiving state. if (!more) { receiving_reply = true; message_begins = true; } return 0; }我们先不管xreq_t::xsend(2)函数的细节,我们会在后面的章节会讲,这儿就先理解成发送消息,那么这边的代码就很容易理解了,就是加上在原消息前面加上empty message part发送。此外还有通过receiving_reply和message_begins的这两个flag来控制消息发送和接受。
2. REP接收消息:
REP接收消息的时候会一直读消息的每一个part,知道找到emtpy message part,并且把empty message part上面的消息头(包括empty message part),发送到相应的输出的管道中,来作reply消息的头。
源代码如下:
int zmq::rep_t::xrecv (zmq_msg_t *msg_, int flags_) { // If we are in middle of sending a reply, we cannot receive next request. if (sending_reply) { errno = EFSM; return -1; } if (request_begins) { // Copy the backtrace stack to the reply pipe. bool bottom = false; while (!bottom) { // 读取消息,直到找到empty message part // TODO: What if request can be read but reply pipe is not // ready for writing? // Get next part of the backtrace stack. int rc = xrep_t::xrecv (msg_, flags_); if (rc != 0) return rc; if ((msg_->flags & ZMQ_MSG_MORE)) { // Empty message part delimits the traceback stack. bottom = (zmq_msg_size (msg_) == 0); // 检测是否是empty message part // Push it to the reply pipe. rc = xrep_t::xsend (msg_, flags_); // 发送消息头到reply管道作为reply消息的消息头 zmq_assert (rc == 0); } else { // If the traceback stack is malformed, discard anything // already sent to pipe (we're at end of invalid message). rc = xrep_t::rollback (); // 消息封装格式invalid,rollback消息。 zmq_assert (rc == 0); } } request_begins = false; } // Now the routing info is safely stored. Get the first part // of the message payload and exit. int rc = xrep_t::xrecv (msg_, flags_); // 接收消息 if (rc != 0) return rc; // If whole request is read, flip the FSM to reply-sending state. if (!(msg_->flags & ZMQ_MSG_MORE)) { sending_reply = true; request_begins = true; } return 0; }
看代码可能容易懂点:
int zmq::xrep_t::xrecv (zmq_msg_t *msg_, int flags_)
{
// If there is a prefetched message, return it.
if (prefetched) {
zmq_msg_move (msg_, &prefetched_msg);
more_in = msg_->flags & ZMQ_MSG_MORE;
prefetched = false;
return 0;
}
// Deallocate old content of the message.
zmq_msg_close (msg_);
// If we are in the middle of reading a message, just grab next part of it.
if (more_in) {
zmq_assert (inpipes [current_in].active);
bool fetched = inpipes [current_in].reader->read (msg_);
zmq_assert (fetched);
more_in = msg_->flags & ZMQ_MSG_MORE;
if (!more_in) {
current_in++;
if (current_in >= inpipes.size ())
current_in = 0;
}
return 0;
}
// Round-robin over the pipes to get the next message.
for (int count = inpipes.size (); count != 0; count--) {
// Try to fetch new message.
if (inpipes [current_in].active)
prefetched = inpipes [current_in].reader->read (&prefetched_msg);
// If we have a message, create a prefix and return it to the caller.
if (prefetched) {
int rc = zmq_msg_init_size (msg_,
inpipes [current_in].identity.size ());
zmq_assert (rc == 0);
memcpy (zmq_msg_data (msg_), inpipes [current_in].identity.data (),
zmq_msg_size (msg_));
msg_->flags |= ZMQ_MSG_MORE;
return 0;
}
// If me don't have a message, mark the pipe as passive and
// move to next pipe.
inpipes [current_in].active = false;
current_in++;
if (current_in >= inpipes.size ())
current_in = 0;
}
// No message is available. Initialise the output parameter
// to be a 0-byte message.
zmq_msg_init (msg_);
errno = EAGAIN;
return -1;
}
然后收到该消息以后会发送到管道中, 调用xreq_t::xsend(2):
int zmq::xrep_t::xsend (zmq_msg_t *msg_, int flags_)
{
// If this is the first part of the message it's the identity of the
// peer to send the message to.
if (!more_out) {
zmq_assert (!current_out);
// If we have malformed message (prefix with no subsequent message)
// then just silently ignore it.
if (msg_->flags & ZMQ_MSG_MORE) {
more_out = true;
// Find the pipe associated with the identity stored in the prefix.
// If there's no such pipe just silently ignore the message.
blob_t identity ((unsigned char*) zmq_msg_data (msg_),
zmq_msg_size (msg_));
outpipes_t::iterator it = outpipes.find (identity);
if (it != outpipes.end ()) {
current_out = it->second.writer;
zmq_msg_t empty;
int rc = zmq_msg_init (&empty);
zmq_assert (rc == 0);
if (!current_out->check_write (&empty)) {
it->second.active = false;
more_out = false;
current_out = NULL;
rc = zmq_msg_close (&empty);
zmq_assert (rc == 0);
errno = EAGAIN;
return -1;
}
rc = zmq_msg_close (&empty);
zmq_assert (rc == 0);
}
}
int rc = zmq_msg_close (msg_);
zmq_assert (rc == 0);
rc = zmq_msg_init (msg_);
zmq_assert (rc == 0);
return 0;
}
// Check whether this is the last part of the message.
more_out = msg_->flags & ZMQ_MSG_MORE;
// Push the message into the pipe. If there's no out pipe, just drop it.
if (current_out) {
bool ok = current_out->write (msg_);
zmq_assert (ok);
if (!more_out) {
current_out->flush ();
current_out = NULL;
}
}
else {
int rc = zmq_msg_close (msg_);
zmq_assert (rc == 0);
}
// Detach the message from the data buffer.
int rc = zmq_msg_init (msg_);
zmq_assert (rc == 0);
return 0;
}
1. 这儿就会根据identity去选择相应的输出管道发送消息。
2. 但是注意identity的前缀消息部分是不会发送到输出管道中的,只是使用check_write(1)检测管道。
3. REP发送消息:
从上面在REP接收消息的分而析的时候已经知道会将消息头,在这儿是Frame1和Frame2写入到reply的管道中。
因此在REP发送的时候就只要加入Frame 3数据部分发送到reply管道中去就ok了。
我们看一下源代码:
int zmq::rep_t::xsend (zmq_msg_t *msg_, int flags_) { // If we are in the middle of receiving a request, we cannot send reply. if (!sending_reply) { errno = EFSM; return -1; } bool more = (msg_->flags & ZMQ_MSG_MORE); // Push message to the reply pipe. int rc = xrep_t::xsend (msg_, flags_); // 发送消息到reply pipe中 if (rc != 0) return rc; // If the reply is complete flip the FSM back to request receiving state. if (!more) sending_reply = false; return 0; }
而xrep_t::xsend(2)部分在REP接收消息的时候已经看过了,对于消息体会写入相应的管道中, 并且在本条消息全部写完后flush管道。
xrep_t::xsend(2): ... // Push the message into the pipe. If there's no out pipe, just drop it. if (current_out) { bool ok = current_out->write (msg_); zmq_assert (ok); if (!more_out) { current_out->flush (); current_out = NULL; } } ...
REQ接收消息的时候就会收消息, 而消息的第一个部分应该是empty message part,然后再把后面的消息收取返回给caller。
代码如下:
int zmq::req_t::xrecv (zmq_msg_t *msg_, int flags_) { // If request wasn't send, we can't wait for reply. if (!receiving_reply) { errno = EFSM; return -1; } // First part of the reply should be empty message part (stack bottom). if (message_begins) { // empty message part处理 int rc = xreq_t::xrecv (msg_, flags_); if (rc != 0) return rc; zmq_assert (msg_->flags & ZMQ_MSG_MORE); zmq_assert (zmq_msg_size (msg_) == 0); message_begins = false; } int rc = xreq_t::xrecv (msg_, flags_); // 接收消息 if (rc != 0) return rc; // If the reply is fully received, flip the FSM into request-sending state. if (!(msg_->flags & ZMQ_MSG_MORE)) { receiving_reply = false; message_begins = true; } return 0; }
本文通过一个简单的REQ-REP例子并结合源代码分析了zeromq中Request-Reply的相关流程。
这边还没有讲xreq::xsend()和xreq::xrecv()函数,但是其实简单地说就是发送的时候会有load-balance去选择一个对端,而接收的时候会有fair-queueing去选择一个对端先处理。这两个策略从概念上来说类似,都是轮流选择。后面在讲ROUTER和DEALER的时候我们会分析相关代码实现,而你会发现ZMQ_DEALER就是用xreq实现的。
下一篇我们会讲ROUTER和DEALER。敬请期待!希望有兴趣的朋友可以和我联系,一起学习。 [email protected]