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

Posted shiyicode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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的封装的主要内容,如果未能解决你的问题,请参考以下文章

[Java] 非阻塞IO

JAVA——重点非阻塞模型:NIO模型及IO操作步骤

MySQL数据库性能优化之存储引擎选择

Linux编程之select

死磕 NIO— 深入分析Buffer

死磕 NIO— 深入分析Buffer