本文我们主要讲述一下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;
}
2. 从代码中可以看出,接下来我们就会调用encoder::get_data(2)去取得相应的数据,最后将数据通过tcp_socket::write(2)函数发送到内核的socket缓冲区中。
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.
if (!pos && !*data_ && to_write >= buffersize) {// 如果缓冲区还为空,*data_为空并且要写的大小大于等于缓冲区的时候,就使用zero-copy,直接将write_pos指向的内存区域地址返回给上层
*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;
}
......
}
而multipart的消息在发送的时候是作为整体发送的是通过pipe::flush()函数实现的,基本上的概念就是当消息写到管道中去,io_thread的poller当可以读的事件发生时,并不能立即读取管道中的这条消息,一定要改消息调用flush()才能读取。
if (!(msg_->flags & ZMQ_MSG_MORE))
pipe_->flush ();
这个也就表明了flush()的调用时机。
希望有兴趣的朋友可以和我联系,一起学习。 kaka11.chen@gmail.com