Epoll-ET模式下非阻塞读写之Buffer的封装

先说说Epoll的ET模式

epoll默认的模式是LT,要说ET不得不提到LT,LT与ET的区别可以用一句话概括:
LT模式下只要socket处于可读状态(添加EPOLLIN事件时)或可写状态(添加EPOLLOUT事件时),就会一直返回其socket。
ET模式下在第一次返回socket后,只有当socket由不可写到可写(添加EPOLLIN事件时)或由不可读到可读(添加EPOLLOUT事件时),才会返回其
socket。

进一步捋一下

由上可得,我们对ET操作必须要做到以下条件:
如果读:必须要将缓冲区的内容全部读出,即读到缓冲区空为止
如果写:一直写,知道需写的数据写完或是缓冲区满为止。
** 要做到以上要求,我们必须使用非阻塞套接字,否则socket会常常处在阻塞情况下,从而导致其他套接字饿死的情况发生。 ** (何谓饿死,程序阻塞在当前套接字的操作上,而其他套接字根本没有机会进行操作)

为什么我们需要Buffer

** TCP 是一个无边界的字节流协议,接收方必须要处理“收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据”等等情况。 **
如果是阻塞模式下,上面的情况根本不需要考虑,因为只要你recv,数据最终一定会过来,但代价就是你会一直阻塞在那里,我们编写服务器当然要尽可能的避免这种情况的
出现,所以我们使用非阻塞套接字。
然而,非阻塞模式下,我们就必须要解决上面的情况。
** 那么,想想,如果给你一个内存空间,你读取数据时,无论取多少,都先放进这个空间,而要处理时从这个空间取数据即可。 **
** 有了这个空间,无论是消息不完整还是多个消息,我们都得以解决。 **
** 写也是一样的,当你向socket发送数据时,如果发送了一部分,缓冲区满了,此时我们该怎么办呢,只好一直阻塞,直至socket变回可写,再将剩余的数据全部写入。显然,代价还是阻塞,如果我们不想阻塞,仍然需要一个类似上面的空间。 **
这个空间,就是我们说的buffer,也可以叫做用户缓冲区。

Buffer的实现思路

首先,我们希望保证任何时候向buffer里面添加数据,都可以添加成功,也就是说,我们的buffer的空间需要足够大,但我们又不能确定一个固定的数值,因为
如果数字设定的小了,还是会出现添加失败的情况,但如果设置的大了,又会导致大量空间的浪费。
所以我们将空间设置为可变的,用vector来保存,因为vector空间增长是以2的幂的形式扩展,很高效。
用两个指针或是变量来作为读标志和写标志,如下图所示。 图片摘自Muduo 设计与实现之一:Buffer 类的设计 )

这张图画的相当的清晰明了,一目了然,就不再具体描述了。
另外有两点优化,第一点是当Buffer内没有数据的时候(也就是readindex=writeindex时),要将两个标志全部归零,以免空间一直无限制增长下去
,前面的空间反而浪费了。
第二点是,要添加数据时,如果剩余的空间不够(writeable),而加上前面空闲的空间(prependable+writeable)能够放下的话,将数据移动
到buffer起始位置,以避免一次空间的增长。

代码

//
//  Buffer.h
//  QuoridorServer
//
//  Created by shiyi on 2016/12/2.
//  Copyright © 2016年 shiyi. All rights reserved.
//

#ifndef Buffer_H
#define Buffer_H

#include <stdio.h>
#include <iostream>
#include <vector>

using namespace std;

class Buffer
{
public:
    Buffer() : m_widx(0), m_ridx(0)
    {}

    ~Buffer(){}

    void init()
    {
        m_widx = m_ridx = 0;
        m_buf.clear();
    }

    //增加内容
    void PutData(char *data, int len)
    {
        //如果调整空间后足够存放,则进行调整
        int capa = m_buf.capacity();
        if(capa < m_widx + len && capa > len + m_widx - m_ridx)
            adjust();

        for(int i = 0; i < len; i++)
            m_buf.push_back(data[i]);

        m_widx += len;
    }

    //返回获取的包的大小,数据不完整返回-1
    int GetData(char* data, int len)
    {
        len = min(m_widx-m_ridx, len);

        for(int i = 0; i < len; i++)
        {
            if(m_ridx >= m_widx)
                break;
            data[i] = m_buf[m_ridx++];
        }

        if(m_ridx >= m_widx)
        {
            m_ridx = m_widx = 0;
            m_buf.clear();
        }
        return len;
    }

private:

    //将数据移至容器头部,充分利用空间
    void adjust()
    {
        vector<char> t(m_buf.begin()+m_ridx, m_buf.begin()+m_widx);
        m_widx -= m_ridx;
        m_ridx = 0;

        m_buf.clear();

        for(int i=0; i<m_widx; i++)
            m_buf.push_back(t[i]);
    }

private:

    int m_ridx;
    int m_widx;
    std::vector<char> m_buf;
};

#endif /* Buffer_H */

参考博客 Muduo 设计与实现之一:Buffer 类的设计


Epoll-ET模式下非阻塞读写之Buffer的封装
https://shiyi.threebody.xyz/posts/20402.html
作者
Yi Shi
发布于
2016年12月3日
许可协议