zeromq源代码分析4------encoder,decoder,multipart_message

本文我们主要讲述一下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.
                // 如果缓冲区还为空,*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的缓冲区中,最后处理缓冲区的数据。
我们先看如何获得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;
            }
        }

这边的代码的意思基本就是:
1.从缓冲区拷贝给数据到read_pos所指的内存空间,当缓冲区满了之后才返回。
2.而状态机的基本流程如下:
one_byte_size_ready() ---> flags_ready() --->message_ready() ---> one_byte_size_ready() ...
或者
one_byte_size_ready() ---> eight_byte_size_ready()  ---> flags_ready() --->message_ready() ---> one_byte_size_ready() ...
就是先读一个字节,如果是0xff那么就是需要再读8个字节得到size,否则该字节就是size,然后读flags,最后读取body。在这些步骤中会构造出一个zmq_msg_t类型的消息对象,并且加入header(size + flags)和body,最后发到相应的管道中去。
这些代码可以自己去看相应函数,在这里我就不贴了。。。
3.当 data_ == read_pos时说明是采用zero-copy,因此不拷贝直接跳转状态。

三. multipart_message:
multipart message基本的概念就是发送消息的时候如果设置flag为ZMQ_SNDMORE表明后面的消息和前面是连在一起的。
zmq_send (socket, &message, ZMQ_SNDMORE);
…
zmq_send (socket, &message, ZMQ_SNDMORE);
…
zmq_send (socket, &message, 0);
在接受消息时通过调用zmq_getsockopt(4)函数查看ZMQ_RCVMORE的设置的时候,如果发觉后面还有消息的话则继续收。
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
}
通过代码分析可以看到主要是通过设置消息的flag为ZMQ_MSG_MORE来实现的,当发送的时候会添加这个flag,当收到消息的时候会看看消息header中的flags中有木有设置该flag,然后设置到zeromq的socket option中去,让调用zmq_getsockopt(4)的时候能够得到该属性。

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()的调用时机。

四: 总结:
本文描述了encoder,decoder以及multipar_message的概念和实现。我们基本了解了poller的读写事件发生时通过读写管道中的消息发送给内核socket缓冲区来收发消息的过程。主要有以下几点:
1. encoder是一个从管道中读取消息然后组装成header(size + flags) + body的binary data给内核socket缓冲区来发送消息的机制。其中使用了缓冲区,状态机和zero-copy的机制来实现和优化。
2. decoder是一个从内核socket缓冲区读取binary data然后组装成zmq_msg_t类型的msg对象最后放入管道的机制。其中也使用了缓冲区,状态机和zero-copy的机制来实现和优化。
3. multipart消息可以将多个消息联合成一个消息发送,主要实现方法是使用pipe的flush()函数。

因此接下去我们有理由在下一篇文章中看看pipe的具体实现,敬请期待!

希望有兴趣的朋友可以和我联系,一起学习。 kaka11.chen@gmail.com

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值