select、poll、epoll详解

目录

引言

背景知识

五中IO模型

同步VS异步

取代异步IO的IO方式

图示IO模型

非阻塞IO

实现函数SetNoBlock

I/O多路转接之select

初识select

SelectServer实现

socket就绪条件

select的缺陷

poll服务器

poll的用法

pollserver

epoll

1.epoll的接口

2.原理

对比总结select, poll, epoll之间的优点和缺点(重要, 面试中常见).

select

优点:

缺点:

poll

优点:

缺点:

epoll

优点:

缺点:

EpollServer

Epoll的LT模式与ET模式

Epoll总结

其他注意点


引言

在互联网的黄金时代,网络服务的需求日益增长,如何高效地处理成千上万的并发连接成为服务器端编程的一大挑战。传统的多线程模型在应对高并发场景时显得力不从心,资源消耗巨大且难以维护。在这种背景下,I/O多路转接技术应运而生,其中select函数作为这一技术的代表,以其独特的优势在服务器端编程领域占据了一席之地。本文将带您深入了解select,探讨其在处理多路I/O时的原理与应用,以及如何在现代网络编程中发挥其重要作用。
本文核心:

1.初识各种IO模型 

2.详细讲解多路转接模型
 

背景知识

IO除了数据在本地的拷贝,还包括网络层面的IO。网络层面的IO就是数据的接收和发送,而IO的效率是十分重要的。

 在网络IO中,其实更多的时间我们并不是在进行数据的拷贝,而是在进行数据的等待---即等待读写资源就绪。

为了让IO的效率更加高效,我们将对等待的条件进行优化,从而使得IO效率提升。

其实scanf也一直在等,等待键盘输入资源就绪

读等:接收缓冲区是否有数据

写等:接收缓冲区是否有数据

为什么要写多线程:可以同时等待多个任务,那么IO等待是并行的,间接提高IO效率。

五中IO模型

我们用钓鱼举例。钓鱼的过程其实是:钓鱼 = 等鱼 + 上鱼(鱼咬钩)。我们以此为模型,进行IO知识的讲解。

在湖边,有这么几个人在钓鱼:张三 李四 王五 赵六 田七

张三:一直盯着鱼漂、钓上鱼来,才去干别的,否则一直盯着鱼漂 --- 阻塞式IO

李四:刷会抖音,再看一眼鱼漂,监测到鱼漂没有鱼,直接返回,不阻塞   ---- 非阻塞轮询IO

王五:往鱼漂上放一个铃铛,把鱼竿放在那里,直接不管了,直到铃铛响了,才回去收杆。 ---- 信号驱动式IO

赵六:放了100个鱼竿,赵六来回踱步,检测哪个鱼竿咬钩。  ---- 多路复用/多路转接

田七:喜欢鱼,找了秘书小王,小王去钓鱼,田七回到公司开会。小王钓上鱼之后,交给田七。

田七没有等鱼,所以田七不是严格的钓鱼者,而是钓鱼任务的分发者。----异步IO

田七在开会,没有钓鱼而这个小王一般就是OS!。田七给OS发配1M大小的缓冲区支配资源,以及一个可以通知田七的方式。当OS有数据之后,就把数据放在缓冲区中。田七只是用数据,并不参与IO。


在IO上,阻塞与非阻塞没有区别,因为等的时间是一样的,但是对于整个工程而言,由于非阻塞干了其他的事情,所以非阻塞的效率更高。

IO方式中,我们最常使用(效率最高)的是多路转接。为什么不采用田七的方式:代码复杂、IO逻辑混乱

同步VS异步

同步VS异步:最大的区别是就看你有没有参与IO:有没有等,有没有拷贝

异步IO的本质是:不参与IO,只是发起IO。

这个同步IO和线程同步没有关系,就像是老婆和老婆饼。

线程同步是借助条件变量wait实现了,只不过是一种谁先谁后的关系。

取代异步IO的IO方式

  1. 同步I/O:

    • 阻塞I/O: 在发起I/O请求后,线程会被阻塞,直到I/O操作完成。这种方式简单但效率低下,因为线程在等待I/O完成期间无法执行其他任务。
    • 非阻塞I/O: 线程在发起I/O请求后可以立即返回,但需要定期轮询检查I/O操作是否完成。这种方式避免了线程阻塞,但轮询会消耗CPU资源。
  2. 多路复用I/O (Multiplexed I/O):

    • select: 允许程序同时监视多个文件描述符,等待一个或多个变得“就绪”(可读、可写或有异常)。
    • poll: 类似于select,但解决了select的一些限制,如文件描述符数量上限。
    • epoll (Linux特有): 提供了更高效的I/O事件通知机制,允许注册事件并仅在事件发生时才通知应用程序。
    • kqueue (BSD特有): 类似于epoll,但用于BSD系统。
  3. 信号驱动I/O (Signal-Driven I/O):

    • 允许I/O操作由内核信号通知完成,而不是通过轮询或阻塞。当文件描述符就绪时,内核会发送一个SIGIO信号。
  4. 基于线程/进程的I/O:

    • 线程池: 使用一组预先创建的线程来处理I/O操作,可以减少线程创建和销毁的开销。
    • 进程池: 类似于线程池,但是使用进程而不是线程。

图示IO模型

阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式

非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.

非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符 , 这个过程称为 轮询 . 这对CPU来说是较大的浪费 , 一般只有特定场景下才使用。

信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作

IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于 IO多路转接能够同时等待多个文件描述符的就绪状态
异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
任何IO过程中, 都包含两个步骤. 第一是 等待, 第二是 拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少。
同步通信 vs 异步通信
同步和异步关注的是消息通信机制 .
所谓同步,就是在发出一个 调用时,在没有得到结果之前,该 调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由 调用者主动等待这个 调用的结果;
异步则是相反, 调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在 调用发出后, 被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
另外 , 我们回忆在讲多进程多线程的时候 , 也提到同步和互斥 . 这里的同步通信和进程之间的同步是完全不想干的概念。

非阻塞IO

fcntl
一个文件描述符 , 默认都是阻塞 IO.
函数原型如下 .
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
传入的 cmd 的值不同 , 后面追加的参数也不相同,fcntl 函数有 5 种功能 :
我们此处只是用第三种功能 , 获取 / 设置文件状态标记 , 就可以将一个文件描述符设置为非阻塞 .

实现函数SetNoBlock

基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞

void SetNoBlock(int fd) { 
 int fl = fcntl(fd, F_GETFL); 
 if (fl < 0) { 
 perror("fcntl");
 return; 
 }
 fcntl(fd, F_SETFL, fl | O_NONBLOCK); 
}
使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.
在轮询中,将文件描述符设置为非阻塞就可以实现立即返回。
recv和read大体一致,但是recv多了一个参数,这个flag我们之前都设置为0,现在将详细介绍这个参数的意义。

我们可以对flag进行参数设置。但是一个个设置太麻烦,存在更加通用的做法。

I/O多路转接之select

初识select

系统提供 select 函数来实现多路复用输入 / 输出模型 .
select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的 ;
程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。

前面的函数在IO的过程中,既负责等,也负责拷贝,而后面这个多路转接则提供了新的系统调用,将等与拷贝进行分离

select 的函数原型如下 : #include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
 fd_set *exceptfds, struct timeval *timeout)

参数解释:

参数nfds是需要监视的最大的文件描述符值+1;
rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;
参数timeout为结构timeval,用来设置select()的等待时间。
参数 timeout 取值:
NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回
关于 fd_set结构
其实这个结构就是一个整数数组 , 更严格的说 , 是一个 " 位图 ". 使用位图中对应的位来表示要监视的文件描述符 .
提供了一组操作fd_set的接口, 来比较方便的操作位图.
我们可以用这个指令去查看timeval结构
        
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0
函数返回值:
执行成功则返回文件描述词状态已改变的个数。
如果返回0代表在描述词状态改变前已超过timeout时间,没有返回。
当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的
值变成不可预测。
错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足

SelectServer实现

主函数

#include "SelectServer.hpp"
#include <memory>

int main()
{
    // std::cout <<"fd_set bits num : " << sizeof(fd_set) * 8 << std::endl;

    std::unique_ptr<SelectServer> svr(new SelectServer());
    svr->Init();
    svr->Start();

    return 0;
}

服务器功能的实现

#pragma once

#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"

using namespace std;

static const uint16_t defaultport = 8888;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1;

class SelectServer
{
public:
    SelectServer(uint16_t port = defaultport) : _port(port)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            fd_array[i] = defaultfd;
            // std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
        }
    }
    
    bool Init()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();

        return true;
    }

    void Accepter()     //连接管理器
    {
        // 我们的连接事件就绪了
        std::string clientip;
        uint16_t clientport = 0;
        int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会
        if (sock < 0) return;
        lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);

        // sock -> fd_array[]
        int pos = 1;
        for (; pos < fd_num_max; pos++) // 第二个循环:将连接维护到数组中(两种设计模式:1.fd = 下标,fd具有独一无二的性质 2.找到为空的位置,直接插入)
        {
            if (fd_array[pos] != defaultfd)
                continue;
            else
                break;
        }
        if (pos == fd_num_max)
        {
            lg(Warning, "server is full, close %d now!", sock);
            close(sock);
        }
        else
        {
            fd_array[pos] = sock;
            //PrintFd();
            // TODO
        }
    }


    void Recver(int fd, int pos)
    {
        // demo
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); 
        if (n > 0)
        {
            buffer[n] = 0;
            cout << "get a messge: " << buffer << endl;
        }
        else if (n == 0)
        {
            lg(Info, "client quit, me too, close fd is : %d", fd);
            close(fd);
            fd_array[pos] = defaultfd; // 这里本质是从select中移除
        }
        else
        {
            lg(Warning, "recv error: fd is : %d", fd);
            close(fd);
            fd_array[pos] = defaultfd; // 这里本质是从select中移除
        }
    }


    void Dispatcher(fd_set &rfds)
    {
        for (int i = 0; i < fd_num_max; i++) // 这是第三个循环
        {
            int fd = fd_array[i];
            if (fd == defaultfd)
                continue;

            if (FD_ISSET(fd, &rfds))    //借助辅助数组传递fd信息。
            {
                if (fd == _listensock.Fd())
                {
                    Accepter(); // 连接管理器
                }
                else // non listenfd
                {
                    Recver(fd, i);
                }
            }
        }
    }

    void Start()
    {
        int listensock = _listensock.Fd();
        fd_array[0] = listensock;

        while (true)
        {
            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd = fd_array[0];

            for (int i = 0; i < fd_num_max; i++) // 第一次循环:更新maxfd   将辅助数组的fd_array[i]加入到rfds中
            {
                if (fd_array[i] == defaultfd)
                    continue;
                FD_SET(fd_array[i], &rfds);
                if (maxfd < fd_array[i])
                {
                    maxfd = fd_array[i];
                    lg(Info, "max fd update, max fd is: %d", maxfd);
                }
            }
        

        
            // accept?不能直接accept!检测并获取listensock上面的事件,新连接到来,等价于读事件就绪
            // struct timeval timeout = {1, 0}; // 输入输出,可能要进行周期的重复设置

            struct timeval timeout = {0, 0};  输入输出,可能要进行周期的重复设置

            // 如果事件就绪,上层不处理,select会一直通知你!
            // select告诉你就绪了,接下来的一次读取,我们读取fd的时候,不会被阻塞
            // rfds: 输入输出型参数。 1111 1111 -> 0000 0000

            int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout /*nullptr*/); 

            switch (n)
            {
                case 0:
                    cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
                    break;
                case -1:
                    cerr << "select error" << endl;
                    break;
                default:
                    // 有事件就绪了,TODO
                    cout << "get a new link!!!!!" << endl;
                    Dispatcher(rfds); // 就绪的事件和fd你怎么知道只有一个呢???
                    break;
            }

        }
    }


    ~SelectServer()
    {
        _listensock.Close();
    }
private:
    Sock _listensock;
    uint16_t _port;
    int fd_array[fd_num_max];   // 辅助数组, 用户维护的!
    // int wfd_array[fd_num_max];
};

代码剖析:

这段代码实现了一个基于 `select` 的服务器,用于处理多个客户端连接。以下是对代码的详细剖析:

### 头文件和命名空间
```cpp
#pragma once

#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"

using namespace std;
```
- `#pragma once`:防止头文件被多次包含。
- 包含了必要的头文件,如 `iostream`、`sys/select.h`、`sys/time.h` 以及自定义的 `Socket.hpp`。
- 使用 `std` 命名空间。

### 常量定义
```cpp
static const uint16_t defaultport = 8888;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1;
```
- `defaultport`:默认端口号为 8888。
- `fd_num_max`:最大文件描述符数量,由 `fd_set` 的大小决定。
- `defaultfd`:默认的文件描述符值为 -1,表示无效或未使用。

### SelectServer 类
#### 构造函数
```cpp
SelectServer(uint16_t port = defaultport) : _port(port)
{
    for (int i = 0; i < fd_num_max; i++)
    {
        fd_array[i] = defaultfd;
    }
}
```
- 初始化监听端口 `_port`。
- 将 `fd_array` 数组中的所有元素初始化为 `defaultfd`。

#### Init 方法
```cpp
bool Init()
{
    _listensock.Socket();
    _listensock.Bind(_port);
    _listensock.Listen();

    return true;
}
```
- 创建套接字并绑定到指定端口,然后开始监听。

#### Accepter 方法
```cpp
void Accepter()
{
    std::string clientip;
    uint16_t clientport = 0;
    int sock = _listensock.Accept(&clientip, &clientport);
    if (sock < 0) return;
    lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);

    int pos = 1;
    for (; pos < fd_num_max; pos++)
    {
        if (fd_array[pos] != defaultfd)
            continue;
        else
            break;
    }
    if (pos == fd_num_max)
    {
        lg(Warning, "server is full, close %d now!", sock);
        close(sock);
    }
    else
    {
        fd_array[pos] = sock;
    }
}
```
- 接受新的客户端连接。
- 如果连接成功,将其添加到 `fd_array` 中。如果服务器已满,则关闭新连接。

#### Recver 方法
```cpp
void Recver(int fd, int pos)
{
    char buffer[1024];
    ssize_t n = read(fd, buffer, sizeof(buffer) - 1); 
    if (n > 0)
    {
        buffer[n] = 0;
        cout << "get a messge: " << buffer << endl;
    }
    else if (n == 0)
    {
        lg(Info, "client quit, me too, close fd is : %d", fd);
        close(fd);
        fd_array[pos] = defaultfd;
    }
    else
    {
        lg(Warning, "recv error: fd is : %d", fd);
        close(fd);
        fd_array[pos] = defaultfd;
    }
}
```
- 从指定的文件描述符读取数据。
- 根据读取结果处理不同的情况:正常读取、客户端断开连接、读取错误。

#### Dispatcher 方法
```cpp
void Dispatcher(fd_set &rfds)
{
    for (int i = 0; i < fd_num_max; i++)
    {
        int fd = fd_array[i];
        if (fd == defaultfd)
            continue;

        if (FD_ISSET(fd, &rfds))
        {
            if (fd == _listensock.Fd())
            {
                Accepter(); // 处理新的连接请求
            }
            else // non listenfd
            {
                Recver(fd, i); // 处理客户端发送的数据
            }
        }
    }
}
```
- 遍历 `fd_array`,检查哪些文件描述符有事件就绪。
- 如果监听套接字有事件就绪,调用 `Accepter` 处理新的连接。
- 如果其他文件描述符有事件就绪,调用 `Recver` 处理客户端数据。

#### Start 方法
```cpp
void Start()
{
    int listensock = _listensock.Fd();
    fd_array[0] = listensock;

    while (true)
    {
        fd_set rfds;
        FD_ZERO(&rfds);
        int maxfd = fd_array[0];

        for (int i = 0; i < fd_num_max; i++)
        {
            if (fd_array[i] == defaultfd)
                continue;
            FD_SET(fd_array[i], &rfds);
            if (maxfd < fd_array[i])
            {
                maxfd = fd_array[i];
                lg(Info, "max fd update, max fd is: %d", maxfd);
            }
        }
    
        struct timeval timeout = {0, 0}; // 不阻塞等待
        int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout); 

        switch (n)
        {
            case 0:
                cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
                break;
            case -1:
                cerr << "select error" << endl;
                break;
            default:
                cout << "get a new link!!!!!" << endl;
                Dispatcher(rfds); // 处理就绪的事件和文件描述符
                break;
        }
    }
}
```
- 主循环:不断调用 `select` 等待文件描述符的事件。
- 更新 `rfds` 集合和 `maxfd`。
- 根据 `select` 返回值处理不同的事件情况。
- 调用 `Dispatcher` 处理就绪的文件描述符。

#### 析构函数
```cpp
~SelectServer()
{
    _listensock.Close();
}
```
- 关闭监听套接字。

socket就绪条件

读就绪

socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;

socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;

监听的socket上有新的连接请求;
socket上有未处理的错误;

写就绪

socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
socket使用非阻塞connect连接成功或失败之后;
socket上有未读取的错误;
异常就绪
socket上收到带外数据. 关于带外数据, 和TCP紧急模式相关(回忆TCP协议头中, 有一个紧急指针的字段)。

select的缺陷

连接获取上来之后,不处理IO事件,那么会一直提醒你IO就绪。

select会检测文件描述符在内核中的数据信息,只有将数据全部取走才不会重复监听,而read读取一部分,没有全部读取完成,select仍然会因为这个fd而暂停等待

这其其实牵扯到了内核的数据结构,比如struct file以及文件缓冲区。

需要注意的是:

1.在内核中,每个文件描述符都有自己的缓冲区。因此在后续的Server的完善中,在最终的reactor版本,将采用 1 fd 1缓冲区的策略去实现。

2.fd_set最大1024个(系统调用取决于系统的实现)

3.由于牵扯到不同函数中文件描述符的传递,所以使用辅助数组(上层维护)

select缺陷:

5.用户层需要额外去维护数组。

poll服务器

为了解决select的短板---poll的方式多路转接。

poll解决了select:1.输入输出型参数重复 2.导致每次都要对fd进行重置的问题。

参数说明
fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合.
nfds表示 fds数组的长度.
timeout表示poll函数的超时时间, 单位是毫秒(ms).

在用户区,仍然需要手动维护数组,只不过数组变成了pollfd【size】。

events revents 的取值 :
可见event与revent也是一个位图。
event是用户设置的索要关心的事件,而revent则是就绪的用户关心的事件。
返回结果
返回值小于0, 表示出错;
返回值等于0, 表示poll函数等待超时;
返回值大于0, 表示poll 由于监听的文件描述符就绪而返回.

poll的用法

由于将事件设置进位图中,所以采用了外部 按位与& 的用法。

同样,事件就绪之后,我们需要采用手动设置到维护的数组中的方式,让后续的等待去监视连接是否就绪。

但是poll仍然避免不了大量的遍历数组的操作,因此性能最优的epoll产生了

pollserver

#pragma once

#include <iostream>
#include <poll.h>
#include <sys/time.h>
#include "Socket.hpp"

using namespace std;

static const uint16_t defaultport = 8888;
static const int fd_num_max = 64;
int defaultfd = -1;
int non_event = 0;

class PollServer
{
public:
    PollServer(uint16_t port = defaultport) : _port(port)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            _event_fds[i].fd = defaultfd;
            _event_fds[i].events = non_event;
            _event_fds[i].revents = non_event;

            // std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
        }
    }
    bool Init()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();

        return true;
    }
    void Accepter()
    {
        // 我们的连接事件就绪了
        std::string clientip;
        uint16_t clientport = 0;
        int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会
        if (sock < 0) return;
        lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);

        // sock -> fd_array[]
        int pos = 1;
        for (; pos < fd_num_max; pos++) // 第二个循环
        {
            if (_event_fds[pos].fd != defaultfd)
                continue;
            else
                break;
        }
        if (pos == fd_num_max)
        {
            lg(Warning, "server is full, close %d now!", sock);
            close(sock);
            // 扩容
        }
        else
        {
            // fd_array[pos] = sock;
            _event_fds[pos].fd = sock;
            _event_fds[pos].events = POLLIN;
            _event_fds[pos].revents = non_event;
            PrintFd();
            // TODO
        }
    }
    void Recver(int fd, int pos)
    {
        // demo
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
        if (n > 0)
        {
            buffer[n] = 0;
            cout << "get a messge: " << buffer << endl;
        }
        else if (n == 0)
        {
            lg(Info, "client quit, me too, close fd is : %d", fd);
            close(fd);
            _event_fds[pos].fd = defaultfd; // 这里本质是从select中移除
        }
        else
        {
            lg(Warning, "recv error: fd is : %d", fd);
            close(fd);
            _event_fds[pos].fd = defaultfd; // 这里本质是从select中移除
        }
    }
    void Dispatcher()
    {
        for (int i = 0; i < fd_num_max; i++) // 这是第三个循环
        {
            int fd = _event_fds[i].fd;
            if (fd == defaultfd)
                continue;

            if (_event_fds[i].revents & POLLIN)
            {
                if (fd == _listensock.Fd())
                {
                    Accepter(); // 连接管理器
                }
                else // non listenfd
                {
                    Recver(fd, i);
                }
            }
        }
    }
    void Start()
    {
        _event_fds[0].fd = _listensock.Fd();
        _event_fds[0].events = POLLIN;
        int timeout = 3000; // 3s
        for (;;)
        {
            int n = poll(_event_fds, fd_num_max, timeout);
            switch (n)
            {
            case 0:
                cout << "time out... " << endl;
                break;
            case -1:
                cerr << "poll error" << endl;
                break;
            default:
                // 有事件就绪了,TODO
                cout << "get a new link!!!!!" << endl;
                Dispatcher(); // 就绪的事件和fd你怎么知道只有一个呢???
                break;
            }
        }
    }
    void PrintFd()
    {
        cout << "online fd list: ";
        for (int i = 0; i < fd_num_max; i++)
        {
            if (_event_fds[i].fd == defaultfd)
                continue;
            cout << _event_fds[i].fd << " ";
        }
        cout << endl;
    }
    ~PollServer()
    {
        _listensock.Close();
    }

private:
    Sock _listensock;
    uint16_t _port;
    struct pollfd _event_fds[fd_num_max]; // 数组, 用户维护的!
    // struct pollfd *_event_fds;

    // int fd_array[fd_num_max];
    // int wfd_array[fd_num_max];
};

epoll

按照man手册的说法: 是为处理大批量句柄而作了改进的poll.

句柄是用于操作系统资源的唯一标识符,如文件、设备和管道等

句柄是一个数值,由操作系统分配给每个打开的资源,应用程序通过句柄来访问这些资源。在Linux系统中,文件描述符(File Descriptor)就是句柄的一种形式,它是一个非负整数,用于表示已打开的文件、网络连接或其它I/O资源。

在Linux中,epoll是一种高效的I/O多路复用机制,它允许程序监视多个文件描述符,以便在某个文件描述符就绪时执行I/O操作。与传统的select和poll机制相比,epoll能够处理更多的文件描述符,且不会因为文件描述符数量的增加而导致性能下降。这是因为epoll使用了一个就绪队列,只有当文件描述符变为就绪状态时,它才会被加入到这个队列中,从而避免了遍历整个文件描述符集合的性能开销。

需要注意的是:epoll是一个全新的模块,他与文件系统想关联。epoll和poll的关系就像是 老婆和老婆饼的关系。只不过在名字上相似,实际上epoll是一个独立的机制与板块。
对于epoll的学习,我们将采用下述方式进行实现。

1.epoll的接口

epoll 3 个相关的系统调用 .
epoll_create
int epoll_create(int size);
创建一个epoll的句柄.
自从linux2.6.8之后,size参数是被忽略的.
用完之后, 必须调用close()关闭.-----返回值是一个文件描述符。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数.
它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
第一个参数是epoll_create()的返回值(epoll的句柄).
第二个参数表示动作,用三个宏来表示.
第三个参数是需要监听的fd.
第四个参数是告诉内核需要监听什么事
第二个参数的取值 :
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd
struct epoll_event结构如下:
events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里

epoll_wait

返回值:返回已经就绪的events的个数

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件。
参数events是分配好的epoll_event结构体数组.
epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
参数timeout是超时时间 ( 毫秒,0会立即返回,-1是永久阻塞).
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败.

2.原理

epoll属于在文件管理模块,和poll、select完全不是一个东西!

epoll的核心机制就是三个:

1.红黑树:用来管理fd事件

2.就绪队列:用来存储已经就绪的IO事件。检测就绪队列只需要O(1)的复杂度,

3.回调函数:每个fd对应一个注册的回调函数,负责事件的向上交付,将事件添加到就绪队列。

epoll模型与三个函数的解读

epoll_create函数在Linux系统中用于创建一个epoll实例,这个实例可以用来监控多个文件描述符的I/O事件。当调用epoll_create时,内核会为这个epoll实例分配资源,包括你提到的红黑树和就绪队列,但epoll_create本身并不直接关联一个回调函数。

epoll_create会注册epoll模型,将文件描述符部分与这个模型相关联。

epoll_wait将就绪队列的就绪事件调出来。

epoll_ctl负责对红黑树进行增删查改。

如何看待这个红黑树:这个红黑树就是我们之前用户维护数组

如何看待就绪队列:这就就绪队列存放的就是连续的就绪事件(即取即用)。

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。
struct eventpoll{ 
 .... 
 /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ 
 struct rb_root rbr; 
 /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/ 
 struct list_head rdlist; 
 .... 
};

每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来 的事件.

这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度).
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.
这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.
在epoll中,对于每一个事件,都会建立一个

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.
如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1).
总结一下, epoll的使用过程就是三部曲:
调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪;

对比总结select, poll, epoll之间的优点和缺点(重要, 面试中常见).

在Linux系统中,selectpollepoll是三种用于IO多路复用的系统调用,它们允许程序同时监视多个文件描述符,以便它们中的任何一个准备好进行IO操作时能够得到通知。以下是这三种机制的优点和缺点对比总结:

select

优点:
  1. 跨平台select是POSIX标准的一部分,因此在多数Unix-like系统中都得到了支持。
  2. 简单易用select的API相对简单,易于理解和实现。
缺点:
  1. 文件描述符数量限制:通常最大只能监听1024个文件描述符(依赖于具体实现)。
  2. 效率低:每次调用select都需要重新传入文件描述符集合和超时时间,并且需要在返回时重新遍历整个集合来确定哪些文件描述符就绪。
  3. 内核态与用户态数据交换开销大select需要复制整个文件描述符集合到内核空间,当监视的文件描述符很多时,这个开销很大。
  4. 无法直接获取就绪的文件描述符:需要遍历整个文件描述符集合来检查哪些文件描述符就绪。

poll

优点:
  1. 没有文件描述符数量限制poll没有select的文件描述符数量限制问题。
  2. 提供了更丰富的事件类型poll可以监视更多类型的事件。
缺点:
  1. 效率问题:与select类似,poll也有效率问题,每次调用都需要重复传递整个文件描述符集合,并且返回时需要遍历整个集合。
  2. 内核态与用户态数据交换开销大:和select一样,poll也存在这个问题。

epoll

优点:
  1. 没有文件描述符数量限制epoll可以处理大量的文件描述符。
  2. 高效的文件描述符事件通知epoll使用回调机制,不需要像selectpoll那样每次调用时都传递文件描述符集合,因此效率更高。
  3. 边缘触发(ET)模式epoll支持边缘触发,这可以减少事件被触发的次数,提高应用程序的性能。
  4. 减少数据拷贝epoll通过共享内存来减少用户态和内核态之间的数据拷贝次数。
缺点:
  1. 仅限于Linux系统epoll是Linux特有的,不是跨平台的。
  2. 使用稍微复杂epoll的API比selectpoll稍微复杂一些,需要理解ET和LT(水平触发)模式的区别。

总结来说,epoll在现代Linux系统中通常是首选的IO多路复用机制,因为它在处理大量文件描述符和提供高效率的事件通知方面有明显的优势。然而,对于需要跨平台支持的应用程序,或者文件描述符数量不是很多的情况下,selectpoll仍然是可行的选择。在面试中,理解这些优缺点,并根据具体场景选择合适的IO多路复用机制,是展示你深入理解系统编程能力的一个重要方面。

EpollServer

#pragma once

#include "nocopy.hpp"
#include "Log.hpp"
#include <cerrno>
#include <cstring>
#include <sys/epoll.h>

class Epoller : nocopy 
{
public:
    Epoller()
    {
        _epfd = epoll_create(11);
         if (_epfd == -1)
        {
            lg(Error, "epoll_create error: %s", strerror(errno));
        }
        else
        {
            lg(Info, "epoll_create success: %d", _epfd);
        }
    }

    int EpollerWait(struct epoll_event revents[], int num)
    {
        int n = epoll_wait(_epfd, revents, num, /*_timeout 0*/ -1);
        return n;
    }


    int EpllerUpdate(int oper, int sock, uint32_t event)
    {
        int n = 0;
        if (oper == EPOLL_CTL_DEL)
        {
            n = epoll_ctl(_epfd, oper, sock, nullptr);
            if (n != 0)
            {
                lg(Error, "epoll_ctl delete error!");
            }
        }
        else
        {
            // EPOLL_CTL_MOD || EPOLL_CTL_ADD
            struct epoll_event ev;
            ev.events = event;      //关注事件的位图
            ev.data.fd = sock; // 目前,方便我们后期得知,是哪一个fd就绪了!

            n = epoll_ctl(_epfd, oper, sock, &ev);
            if (n != 0)
            {
                lg(Error, "epoll_ctl error!");
            }
        }
        return n;
    }

    ~Epoller()  //关闭底层注册的模型
    {
        if (_epfd >= 0)
            close(_epfd);
    }


private:
    int _epfd;
    int _timeout{3000};
};
#pragma once

#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Epoller.hpp"
#include "Log.hpp"
#include "nocopy.hpp"

uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);
#pragma once

#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Epoller.hpp"
#include "Log.hpp"
#include "nocopy.hpp"

uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);

class EpollServer : public nocopy
{
    static const int num = 64;

public:
    EpollServer(uint16_t port)
        : _port(port),
          _listsocket_ptr(new Sock()),
          _epoller_ptr(new Epoller())
    {
    }
    void Init()
    {
        _listsocket_ptr->Socket();
        _listsocket_ptr->Bind(_port);
        _listsocket_ptr->Listen();

        lg(Info, "create listen socket success: %d\n", _listsocket_ptr->Fd());
    }
    void Accepter() //连接管理器
    {
        // 获取了一个新连接
        std::string clientip;
        uint16_t clientport;
        int sock = _listsocket_ptr->Accept(&clientip, &clientport);
        if (sock > 0)
        {
            // 我们能直接读取吗?不能
            _epoller_ptr->EpllerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
            lg(Info, "get a new link, client info@ %s:%d", clientip.c_str(), clientport);
        }
    }
    // for test
    void Recver(int fd)
    {
        // demo
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "get a messge: " << buffer << std::endl;
            // wrirte
            std::string echo_str = "server echo $ ";
            echo_str += buffer;
            write(fd, echo_str.c_str(), echo_str.size());
        }
        else if (n == 0)
        {
            lg(Info, "client quit, me too, close fd is : %d", fd);
            //细节3:连接关闭,需要从红黑树移除连接
            _epoller_ptr->EpllerUpdate(EPOLL_CTL_DEL, fd, 0);
            close(fd);
        }
        else
        {
            lg(Warning, "recv error: fd is : %d", fd);
            _epoller_ptr->EpllerUpdate(EPOLL_CTL_DEL, fd, 0);
            close(fd);
        }
    }
    void Dispatcher(struct epoll_event revs[], int num)
    {
        for (int i = 0; i < num; i++)
        {
            uint32_t events = revs[i].events;
            int fd = revs[i].data.fd;
            if (events & EVENT_IN)
            {
                if (fd == _listsocket_ptr->Fd())    //listensock就绪,就意味着连接事件到来。
                {
                    Accepter();
                }
                else
                {
                    // 其他fd上面的普通读取事件就绪
                    Recver(fd);
                }
            }
            else if (events & EVENT_OUT)
            {
            }
            else
            {
            }
        }
    }
    void Start()
    {
        // 将listensock添加到epoll中 -> listensock和他关心的事件,添加到内核epoll模型中rb_tree.
        _epoller_ptr->EpllerUpdate(EPOLL_CTL_ADD, _listsocket_ptr->Fd(), EVENT_IN);
        struct epoll_event revs[num];
        for (;;)
        {
            int n = _epoller_ptr->EpollerWait(revs, num);   
            if (n > 0)
            {
                // 有事件就绪
                lg(Debug, "event happened, fd is : %d", revs[0].data.fd);
                Dispatcher(revs, n);
            }
            else if (n == 0)
            {
                lg(Info, "time out ...");
            }
            else
            {
                lg(Error, "epll wait error");
            }
        }
    }
    ~EpollServer()
    {
        _listsocket_ptr->Close();
    }

private:
    std::shared_ptr<Sock> _listsocket_ptr;
    std::shared_ptr<Epoller> _epoller_ptr;  //已经进行封装,不需要再关心_epfd
    uint16_t _port;
};

这段代码实现了一个基于 `epoll` 的服务器,用于处理网络连接和数据接收。以下是对代码的详细剖析:

### 类定义与成员变量
```cpp
class EpollServer : public nocopy
{
    static const int num = 64;

public:
    EpollServer(uint16_t port)
        : _port(port),
          _listsocket_ptr(new Sock()),
          _epoller_ptr(new Epoller())
    {
    }
```
- **类继承**:`EpollServer` 继承自 `nocopy`,表示该类不能被拷贝。
- **静态常量**:`num` 是一个静态常量,表示 `epoll` 事件数组的大小。
- **构造函数**:初始化端口号、监听套接字指针和 `epoll` 对象指针。

### 初始化方法
```cpp
void Init()
{
    _listsocket_ptr->Socket();
    _listsocket_ptr->Bind(_port);
    _listsocket_ptr->Listen();

    lg(Info, "create listen socket success: %d\n", _listsocket_ptr->Fd());
}
```
- **创建套接字**:调用 `Socket` 方法创建一个套接字。
- **绑定端口**:将套接字绑定到指定端口。
- **监听**:开始监听连接请求。
- **日志记录**:记录成功创建监听套接字的信息。

### 接受连接的方法
```cpp
void Accepter()
{
    std::string clientip;
    uint16_t clientport;
    int sock = _listsocket_ptr->Accept(&clientip, &clientport);
    if (sock > 0)
    {
        _epoller_ptr->EpllerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
        lg(Info, "get a new link, client info@ %s:%d", clientip.c_str(), clientport);
    }
}
```
- **接受新连接**:调用 `Accept` 方法接受一个新的客户端连接。
- **更新 `epoll`**:将新的连接套接字添加到 `epoll` 中,并关注其读事件。
- **日志记录**:记录新连接的信息。

### 接收数据的方法
```cpp
void Recver(int fd)
{
    char buffer[1024];
    ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
    if (n > 0)
    {
        buffer[n] = 0;
        std::cout << "get a messge: " << buffer << std::endl;
        std::string echo_str = "server echo $ ";
        echo_str += buffer;
        write(fd, echo_str.c_str(), echo_str.size());
    }
    else if (n == 0)
    {
        lg(Info, "client quit, me too, close fd is : %d", fd);
        _epoller_ptr->EpllerUpdate(EPOLL_CTL_DEL, fd, 0);
        close(fd);
    }
    else
    {
        lg(Warning, "recv error: fd is : %d", fd);
        _epoller_ptr->EpllerUpdate(EPOLL_CTL_DEL, fd, 0);
        close(fd);
    }
}
```
- **读取数据**:从文件描述符 `fd` 读取数据到缓冲区 `buffer`。
- **处理数据**:如果读取到的数据长度大于0,则打印消息并回显给客户端。
- **处理连接关闭**:如果读取到的数据长度为0,表示客户端关闭了连接,移除 `epoll` 中的该文件描述符并关闭它。
- **处理错误**:如果读取出错,同样移除 `epoll` 中的该文件描述符并关闭它。

### 事件分发器
```cpp
void Dispatcher(struct epoll_event revs[], int num)
{
    for (int i = 0; i < num; i++)
    {
        uint32_t events = revs[i].events;
        int fd = revs[i].data.fd;
        if (events & EVENT_IN)
        {
            if (fd == _listsocket_ptr->Fd())
            {
                Accepter();
            }
            else
            {
                Recver(fd);
            }
        }
        else if (events & EVENT_OUT)
        {
        }
        else
        {
        }
    }
}
```
- **遍历事件**:遍历所有就绪的事件。
- **处理读事件**:如果是监听套接字有读事件,调用 `Accepter` 方法接受新连接;否则调用 `Recver` 方法处理普通读事件。
- **处理写事件**:目前没有实现写事件的处理逻辑。
- **其他事件**:没有处理其他类型的事件。

### 启动方法
```cpp
void Start()
{
    _epoller_ptr->EpllerUpdate(EPOLL_CTL_ADD, _listsocket_ptr->Fd(), EVENT_IN);
    struct epoll_event revs[num];
    for (;;)
    {
        int n = _epoller_ptr->EpollerWait(revs, num);
        if (n > 0)
        {
            lg(Debug, "event happened, fd is : %d", revs[0].data.fd);
            Dispatcher(revs, n);
        }
        else if (n == 0)
        {
            lg(Info, "time out ...");
        }
        else
        {
            lg(Error, "epll wait error");
        }
    }
}
```
- **添加监听套接字到 `epoll`**:将监听套接字添加到 `epoll` 中,并关注其读事件。
- **无限循环等待事件**:进入无限循环,等待事件发生。
- **处理事件**:如果有事件发生,调用 `Dispatcher` 方法处理这些事件。
- **处理超时和错误**:如果没有事件发生(超时),记录日志;如果出现错误,记录错误日志。

### 析构函数
```cpp
~EpollServer()
{
    _listsocket_ptr->Close();
}
```
- **关闭监听套接字**:在析构函数中关闭监听套接字。

        

Epoll的LT模式与ET模式

水平触发Level Triggered 工作模式
epoll默认状态下就是LT工作模式.
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait
仍然会立刻返回并通知socket读事件就绪.
直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
支持阻塞读写和非阻塞读写
边缘触发 Edge Triggered工作模式:即Δ
如果我们在第 1 步将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志 , epoll 进入 ET 工作模式
当epoll检测到socket上事件就绪时, 必须立刻处理.
如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候,
epoll_wait 不会再返回了.
也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
只支持非阻塞的读写。
select poll 其实也是工作在 LT 模式下 . epoll 既可以支持 LT, 也可以支持 ET。
如果不全部取走,那么出现变化量,还是会报警。
对比 LT ET
LT epoll 的默认行为 . 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.
相当于一个文件描述符就绪之后 , 不会反复被提示就绪 , 看起来就比 LT 更高效一些 . 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理 , 不让这个就绪被重复提示的话 , 其实性能也是一样的 .
另一方面 , ET 的代码复杂程度更高了 .
需要通过fcntl对fd进行非阻塞设置。O_NONBLCOK。
IO效率高:既然程序员每次都需要完全取走数据,那么缓冲区变大,滑动窗口变大,数据发送时,数据的携带能力提高。
将文件描述符设置到内核时,需要额外添加ET选项

Epoll总结

理解 ET 模式和非阻塞文件描述符
使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞 . 这个不是接口上的要求 , 而是 " 工程实践 " 上的要求 .
假设这样的场景 : 服务器接受到一个 10k 的请求 , 会向客户端返回一个应答数据 . 如果客户端收不到应答 , 不会发送第二个 10k 请求。
如果服务端写的代码是阻塞式的 read, 并且一次只 read 1k 数据的话 (read 不能保证一次就把所有的数据都读出来, 参考 man 手册的说明 , 可能被信号打断 ), 剩下的 9k 数据就会待在缓冲区。
此时由于 epoll ET 模式 , 并不会认为文件描述符读就绪 . epoll_wait 就不会再次返回 . 剩下的 9k 数据会一直在缓冲区中 . 直到下一次客户端再给服务器写数据 . epoll_wait 才能返回。
但是问题来了 .
服务器只读到 1k 个数据 , 10k 读完才会给客户端返回响应数据 .
客户端要读到服务器的响应, 才会发送下一个请求
客户端发送了下一个请求 , epoll_wait 才会返回 , 才能去读缓冲区中剩余的数据
所以 , 为了解决上述问题 ( 阻塞 read 不一定能一下把完整的请求读完 ), 于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来 .
而如果是 LT 没这个问题 . 只要缓冲区中的数据没读完 , 就能够让 epoll_wait 返回文件描述符读就绪
epoll 的使用场景
epoll 的高性能 , 是有一定的特定场景的 . 如果场景选择的不适宜 , epoll 的性能可能适得其反. 对于多连接 , 且多连接中只有一部分连接比较活跃时 , 比较适合使用 epoll.
例如 , 典型的一个需要处理上万个客户端的服务器 , 例如各种互联网 APP 的入口服务器 , 这样的服务器就很适合 epoll.
如果只是系统内部 , 服务器和服务器之间进行通信 , 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型。

其他注意点

1.

写事件(Write Events)

写事件则通常按需设置,原因如下:

输出缓冲区状态:当发送数据的速度超过了网络链路或者对端处理的速度时,输出缓冲区可能会满。如果此时继续写入,可能会导致缓冲区溢出。因此,1.当输出缓冲区满时,需要停止关注写事件,直到缓冲区有空余空间。 2.缓冲区常常存在剩余空间,可以直接写入。

按需写入:通常情况下,应用层不需要不断地写入数据,而是有数据要发送时才写入。因此,只有在发送缓冲区有空余且确实有数据要发送时,才需要关注写事件。

资源利用持续关注写事件可能会造成不必要的 CPU 资源浪费epoll持续返回,因为大多数时间可能并不需要写入数据。

正确的策略:

直接写入,如果写入完成,就结束。如果写入完成,但是数据没有写完,outbuffer里还有内容,我们就需要设置对写事件进行关心了,如果写完了,去掉对写事件的关心!

2.给每个文件描述符都定义属于自己的方法与缓冲区(内核中也是这么实现的)

3.#pragma once可以保证防止头文件被重复包含,从而当头文件出现函数的定义时,防止出现函数的重定义问题

4.

这样在读或者写的时候一定就会出错,可以在读写中去排查。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值