本文我们主要讲述一下zeromq中比较核心的类zmq_engine中的几个部件encoder, decoder等以及multipart_message的实现。
zeromq源代码分析3中我们曾经描述了
因为tcp是一种字节流类型的协议,木有边界,所以把该消息边界的制定留给了应用层。通常有两种方式实现:
1. 在传输的数据中添加分隔符。
2. 在每条消息中添加size字段。
而zeromq可以说选择了第二种方式。
今天就来看看真正发送到内核中的socket缓冲区的数据格式。
一. encoder:
encoder是一个将zmq_msg_t类型的消息转换成最终发送到内核中的socket缓冲区中的组件。
主要有以下的数据结构:encoder_base中的: unsigned char *write_pos; // 被写到缓冲区的内存区域的位置指针,指向in_progress的数据区域或者tmpbuf. size_t to_write; // 被写到缓冲区的大小 step_t next; // 状态机中下一个函数 bool beginning; // 是否是multipar_message中的第一个消息或者单个消息 size_t bufsize; // 缓冲区大小 unsigned char *buf; // 缓冲区 encoder中的: struct i_inout *source; // 读写消息的机制,初始化的时候是由zmq_init实现与对端交换identity,初始化完成后是由session实现读管道中的消息。 ::zmq_msg_t in_progress; // 从管道中读取即将被写的消息 unsigned char tmpbuf [10]; // 被写的消息头内存区域(主要是size)
基本上encoder使用了一个状态机来根据状态的迁移执行不同的处理。
主要的流程是:
1. 因为从上几篇文章来说你了解到消息被写到pipe中去,当某一个io_thread的poller轮询到可写的事件时,zmq_engine就被回调out_event(). 代码如下
void zmq::zmq_engine_t::out_event () { // If write buffer is empty, try to read new data from the encoder. if (!outsize) { outpos = NULL; encoder.get_data (&outpos, &outsize); // If IO handler has unplugged engine, flush transient IO handler. if (unlikely (!plugged)) { zmq_assert (ephemeral_inout); ephemeral_inout->flush (); return; } // If there is no data to send, stop polling for output. if (outsize == 0) { reset_pollout (handle); return; } } // If there are any data to write in write buffer, write as much as // possible to the socket. int nbytes = tcp_socket.write (outpos, outsize); // Handle problems with the connection. if (nbytes == -1) { error (); return; } outpos += nbytes; outsize -= nbytes; }
3. encoder基本的工作流程就是从管道中获得message,然后取得消息的长度,将其拼装在消息数据的头部,然后连同消息数据返回给zmq_engine发送。
我们先看看encoder的构造函数:
zmq::encoder_t::encoder_t (size_t bufsize_) : encoder_base_t <encoder_t> (bufsize_), source (NULL) { zmq_msg_init (&in_progress); // Write 0 bytes to the batch and go to message_ready state. next_step (NULL, 0, &encoder_t::message_ready, true); }这边初始化了in_progress为空消息,用来存放从管道中读取的消息。还调用了状态机设置行为函数,设置了下一个状态是message_ready函数。
// Prototype of state machine action. typedef bool (T::*step_t) (); // 状态处理函数的函数指针类型 // This function should be called from derived class to write the data // to the buffer and schedule next state machine action. Set beginning // to true when you are writing first byte of a message. inline void next_step (void *write_pos_, size_t to_write_, step_t next_, bool beginning_) { write_pos = (unsigned char*) write_pos_; // 设置被写的内存区域的位置指针 to_write = to_write_; // 设置被写的大小 next = next_; // 下一个状态处理函数 beginning = beginning_; // 是否是multipar_message中的第一个消息或者单个消息 }
基本上encoder里面当调用指向step_t类型的函数指针的时候,状态就会发生迁移。
状态迁移函数的过程: message_ready()--->size_ready()->message_ready()...。
message_ready(): 通过消息读写机制(读写消息的机制,初始化的时候是由zmq_init实现与对端交换identity,初始化完成后是由session实现读管道中的消息)中读取消息,并且获得其size以及flags,将其放入到tmp_buf中去,等待copy到encoder的缓冲区中或者zero copy(这个等一下我们会讲),最终返回地址给zmq_engine。
size_ready(): 写in_progress中保存的消息data到encoder的缓冲区中或者zero copy(这个等一下我们会讲),最终返回地址给zmq_engine。
尼玛。。。这边好像很能用语言表达清楚,如果在被我弄晕了,还是接下去直接我们看代码吧
bool zmq::encoder_t::message_ready ()
{
// Destroy content of the old message.
zmq_msg_close (&in_progress);
// Read new message. If there is none, return false.
// Note that new state is set only if write is successful. That way
// unsuccessful write will cause retry on the next state machine
// invocation.
if (!source || !source->read (&in_progress)) {
zmq_msg_init (&in_progress);
return false;
}
// Get the message size.
size_t size = zmq_msg_size (&in_progress);
// Account for the 'flags' byte.
size++; // 用于flags
// For messages less than 255 bytes long, write one byte of message size.
// For longer messages write 0xff escape character followed by 8-byte
// message size. In both cases 'flags' field follows.
if (size < 255) {
tmpbuf [0] = (unsigned char) size;
tmpbuf [1] = (in_progress.flags & ~ZMQ_MSG_SHARED);
next_step (tmpbuf, 2, &encoder_t::size_ready,
!(in_progress.flags & ZMQ_MSG_MORE));
}
else {
tmpbuf [0] = 0xff;
put_uint64 (tmpbuf + 1, size);
tmpbuf [9] = (in_progress.flags & ~ZMQ_MSG_SHARED);
next_step (tmpbuf, 10, &encoder_t::size_ready,
!(in_progress.flags & ZMQ_MSG_MORE));
}
return true;
}
这边还有一个zeromq的优化方面的技巧:
1. 对于消息长度小于255,使用一个字节来保存消息长度。
2. 否则就先写0xff, 再用8个字节来保存消息长度。
注意这边因为要保存flags,所以size要加1。
最后通过调用next_step(4)设置被写到encoder缓冲区的地址(tmpbuf保存的header),大小,下一个函数(拼装消息body)以及如果是multi-part message是否后面还有分块的消息。
而size_ready()函数就比较简单了
bool zmq::encoder_t::size_ready () { // Write message body into the buffer. next_step (zmq_msg_data (&in_progress), zmq_msg_size (&in_progress), &encoder_t::message_ready, false); return true; }等到写完最后要发给内核socket缓冲区的消息数据头(size + flags)之后,就写一下body就ok了
通过调用next_step(4)设置被写到encoder缓冲区的地址(in_progress消息的data),大小,下一个函数(去处理下一条消息)以及如果是multi-part message是否后面还有分块的消息。
差不多了,你应该结合源代码略懂了吧,下面来看一下encoder的关键函数,也就是被zmq_engine:: out_event()所调用的函数get_data(3)。
我们先讲一下这个函数基本的工作原理,然后再看代码细节。
该函数主要通过从write_pos指针所指向被写的内存区域的位置这边拷贝给encoder的数据缓冲区,并将数据缓冲区返回给上层调用者。
zeromq这儿还有一个优化的方式就是使用所谓的"ZERO COPY",即不做memcpy(3)。
现在让我们来看一下代码:
inline void get_data (unsigned char **data_, size_t *size_, int *offset_ = NULL) { unsigned char *buffer = !*data_ ? buf : *data_; // 缓冲区指针,如果*data本来就是不为NULL,指向它自身的缓冲区 size_t buffersize = !*data_ ? bufsize : *size_; // 缓冲区大小 size_t pos = 0; // 当前写的位置 if (offset_) *offset_ = -1; // 调整偏移量 while (true) { // If there are no more data to return, run the state machine. // If there are still no data, return what we already have // in the buffer. if (!to_write) { // 没有要被写的数据,构造函数中的初始化情况或者被写完的情况(这种情况说明缓冲区还有空间) if (!(static_cast <T*> (this)->*next) ()) { // 执行状态迁移函数,失败时就返回,说明管道中木有消息了 *data_ = buffer; *size_ = pos; return; } // If beginning of the message was processed, adjust the // first-message-offset. if (beginning) { // 当多部分的消息是第一条的时候,调整该消息的偏移量 if (offset_ && *offset_ == -1) *offset_ = pos; beginning = false; } } // If there are no data in the buffer yet and we are able to // fill whole buffer in a single go, let's use zero-copy. // There's no disadvantage to it as we cannot stuck multiple // messages into the buffer anyway. Note that subsequent // write(s) are non-blocking, thus each single write writes // at most SO_SNDBUF bytes at once not depending on how large // is the chunk returned from here. // As a consequence, large messages being sent won't block // other engines running in the same I/O thread for excessive // amounts of time.// 如果缓冲区还为空,*data_为空并且要写的大小大于等于缓冲区的时候,就使用zero-copy,直接将write_pos指向的内存区域地址返回给上层if (!pos && !*data_ && to_write >= buffersize) {
*data_ = write_pos;
*size_ = to_write;
write_pos = NULL;
to_write = 0;
return;
}
// Copy data to the buffer. If the buffer is full, return.
// 否则就拷贝数据到缓冲区, 直到缓冲区满
size_t to_copy = std::min (to_write, buffersize - pos);
memcpy (buffer + pos, write_pos, to_copy);
pos += to_copy;
write_pos += to_copy;
to_write -= to_copy;
if (pos == buffersize) { // 缓冲区满了才返回
*data_ = buffer;
*size_ = pos;
return;
}
}
}
这边发送数据的策略如下:
1. 如果是有很多小消息(比缓冲区小的消息)在管道中等待读取发送,那么一直会累积到缓冲区满之后才会发送。
2. 如果缓冲区还为空,*data_为空并且要写的大小大于等于缓冲区大小的时候,就使用zero-copy,直接将write_pos指向的内存区域地址返回给上层。
3. 当管道中木有msg的时候会返回。
该策略一直累积消息到encoder的缓冲区满或者当管道木有消息的时候才返回,而对于大于等于缓冲区大小的大消息,直接使用zero-copy技术不做copy返回给上层,这样可以
提高发送的效率,减少sys call和内存拷贝。
二. decoder:
decoder的编写思路基本和ecoder类似,也有状态机来表示操作的状态迁移,只不过从内核socket缓冲区读取消息后将数据放入decoder的缓冲区,最后写到管道中去供应用层读取。有了encoder的分析,我想分析decoder就不那么困难了。
decoder是一个将从内核socket缓冲区收到的数据转换成zmq_msg_t类型的消息的组件。
主要有以下的数据结构:decoder_base中的: unsigned char *read_pos; // 从缓冲区读出的内存区域位置,指向in_progress的数据区域或者tmpbuf size_t to_read; // 从缓冲区读出的大小 step_t next; // 状态机中的下一个函数 size_t bufsize; // 缓冲区大小 unsigned char *buf; // 缓冲区 dncoder中的:struct i_inout *destination; // 读写消息的机制,初始化的时候是由zmq_init实现与对端交换identity,初始化完成后是由session实现读管道中的消息。 unsigned char tmpbuf [8]; // 从缓冲区读出数据到消息头内存区域(主要是size,flags) ::zmq_msg_t in_progress; // 从缓冲区读出数据到此的消息对象
当io_thread的poller轮询到可读事件的时候,我们会调用zmq_engine_t:: in_event()函数。
void zmq::zmq_engine_t::in_event () { bool disconnection = false; // If there's no data to process in the buffer... if (!insize) { // Retrieve the buffer and read as much data as possible. decoder.get_buffer (&inpos, &insize); // 获得decoder中缓冲区的地址和大小 insize = tcp_socket.read (inpos, insize); // 读取内核socket缓冲区的接受到的数据到decoder中的缓冲区 // Check whether the peer has closed the connection. if (insize == (size_t) -1) { insize = 0; disconnection = true; } } // Push the data to the decoder. size_t processed = decoder.process_buffer (inpos, insize); // 处理缓冲区里面的数据 if (unlikely (processed == (size_t) -1)) { disconnection = true; } else { // Stop polling for input if we got stuck. if (processed < insize) { // This may happen if queue limits are in effect or when // init object reads all required information from the socket // and rejects to read more data. if (plugged) reset_pollin (handle); } // Adjust the buffer. inpos += processed; insize -= processed; } // Flush all messages the decoder may have produced. // If IO handler has unplugged engine, flush transient IO handler. if (unlikely (!plugged)) { zmq_assert (ephemeral_inout); ephemeral_inout->flush (); } else { inout->flush (); } if (inout && disconnection) error (); }我们看到基本的流程就是获得decoder的缓冲区地址,然后将内核socket缓冲区所读到的数据读到decoder的缓冲区中,最后处理缓冲区的数据。
// Returns a buffer to be filled with binary data. inline void get_buffer (unsigned char **data_, size_t *size_) { // If we are expected to read large message, we'll opt for zero- // copy, i.e. we'll ask caller to fill the data directly to the // message. Note that subsequent read(s) are non-blocking, thus // each single read reads at most SO_RCVBUF bytes at once not // depending on how large is the chunk returned from here. // As a consequence, large messages being received won't block // other engines running in the same I/O thread for excessive // amounts of time. if (to_read >= bufsize) { // 采用zero-copy *data_ = read_pos; *size_ = to_read; return; } *data_ = buf; *size_ = bufsize; }这边有个优化,如果要读取的数据的大于等于缓冲区的大小,我们就使用zero-copy,不copy到缓冲区,而直接返回read_pos给上层。
// Processes the data in the buffer previously allocated using // get_buffer function. size_ argument specifies nemuber of bytes // actually filled into the buffer. Function returns number of // bytes actually processed. inline size_t process_buffer (unsigned char *data_, size_t size_) { // Check if we had an error in previous attempt. if (unlikely (!(static_cast <T*> (this)->next))) return (size_t) -1; // In case of zero-copy simply adjust the pointers, no copying // is required. Also, run the state machine in case all the data // were processed. if (data_ == read_pos) { // 采用zero-copy read_pos += size_; to_read -= size_; while (!to_read) { if (!(static_cast <T*> (this)->*next) ()) { if (unlikely (!(static_cast <T*> (this)->next))) return (size_t) -1; return size_; } } return size_; } size_t pos = 0; while (true) { // Try to get more space in the message to fill in. // If none is available, return. while (!to_read) { if (!(static_cast <T*> (this)->*next) ()) { if (unlikely (!(static_cast <T*> (this)->next))) return (size_t) -1; return pos; } } // If there are no more data in the buffer, return. if (pos == size_) return pos; // Copy the data from buffer to the message. size_t to_copy = std::min (to_read, size_ - pos); memcpy (read_pos, data_ + pos, to_copy); read_pos += to_copy; pos += to_copy; to_read -= to_copy; } }
zmq_send (socket, &message, ZMQ_SNDMORE); … zmq_send (socket, &message, ZMQ_SNDMORE); … zmq_send (socket, &message, 0);
while (1) { zmq_msg_t message; zmq_msg_init (&message); zmq_recv (socket, &message, 0); // Process the message part zmq_msg_close (&message); int64_t more; size_t more_size = sizeof (more); zmq_getsockopt (socket, ZMQ_RCVMORE, &more, &more_size); if (!more) break; // Last message part }
int zmq::socket_base_t::send (::zmq_msg_t *msg_, int flags_) { ...... // At this point we impose the MORE flag on the message. if (flags_ & ZMQ_SNDMORE) msg_->flags |= ZMQ_MSG_MORE; ...... }
int zmq::socket_base_t::recv (::zmq_msg_t *msg_, int flags_) { ...... // If we have the message, return immediately. if (rc == 0) { rcvmore = msg_->flags & ZMQ_MSG_MORE; if (rcvmore) msg_->flags &= ~ZMQ_MSG_MORE; return 0; } ...... }
if (!(msg_->flags & ZMQ_MSG_MORE)) pipe_->flush ();这个也就表明了flush()的调用时机。
希望有兴趣的朋友可以和我联系,一起学习。 [email protected]