【C++】I/O多路转接详解(一)

news2024/11/18 13:44:53

目录

  • 1. 背景引入
    • 1.1 IO的过程
    • 1.2 五种IO模型
      • 1.2.1 阻塞IO
      • 1.2.2 非阻塞IO
      • 1.2.3 信号驱动IO
      • 1.2.4 IO多路转接
      • 1.2.5 异步IO
    • 1.3 同步通信 与 异步通信
    • 1.4 阻塞 与 非阻塞
      • 1.4.1 阻塞与非阻塞区别
      • 1.4.2 设置非阻塞IO
  • 2. select
    • 2.1 接口使用
    • 2.2 select执行过程
    • 2.3 select代码实践
  • 3. poll
    • 3.1 接口使用
    • 3.2 poll的执行过程
    • 3.3 poll的优点与缺点
    • 3.4 poll代码实践
  • 4. epoll
    • 4.1 接口使用
      • 4.1.1 epoll_create
      • 4.1.2 epoll_ctl
      • 4.1.3 epoll_wait
    • 4.2 epoll 的执行过程
    • 4.3 epoll的优点
    • 4.4 epoll的两种工作方式
      • 4.4.1 水平触发(Level Triggered)
      • 4.4.2 边缘触发(Edge Triggered)
      • 4.4.3 对比LT和ET
      • 4.4.4 理解ET模式和非阻塞
    • 4.5 epoll代码实践

1. 背景引入

1.1 IO的过程

我们先来介绍一下基本的I/O过程:

我们以用户调用read来读取数据为例子:

  1. CPU向磁盘控制器发出指令,并返回
  2. 磁盘控制器收到指令,开始准备数据,将数据放入磁盘控制器的内部缓冲区中,并产生中断
  3. CPU收到中断信号,停止其他工作,将磁盘缓冲区中的数据每次一字节的读入寄存器(PageCache),然后将寄存器中的数据写入内存,整个过程中CPU无法执行其他任务
  4. 数据从内存拷贝到用户缓冲区
    在这里插入图片描述

这就是最原始的IO模型,实际上现在已经做出来许多优化,比如DMA技术,零拷贝等等,这里不是本文的重点,所以不做过多赘述。

这里我们可以总结出,IO 分为两个过程,一个是等待(数据准备)过程,一个是数据拷贝过程(对于read,内核->用户;对于write,用户->内核)。

  • 如何提高IO的效率

在实际的应用场景之中,等待消耗的时间通常是远远高于拷贝的时间的,想要提高IO的效率,很显然,我们需要减少IO过程中等待的比例,换句话说,一个高效的IO,在整个运作周期内,等的比重是很小的,更多的是在进行拷贝。

下面介绍五种常见的IO模型,并将IO过程简化为 等待与拷贝过程,分析其高效性。


1.2 五种IO模型

1.2.1 阻塞IO

在内核将数据准备好之前,系统调用将持续等待,所有的套接字,默认为阻塞方式
在这里插入图片描述

1.2.2 非阻塞IO

如果内核还没有将数据准备好,系统调用仍然会直接返回,并返回EWOULDBLOCK错误码(即底层无数据)

非阻塞IO通常需要程序员使用循环的方式反复尝试读写文件描述符,这个过程被称为 轮询 。显然,这种行为对CPU的消耗较大。

在这里插入图片描述

1.2.3 信号驱动IO

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

在这里插入图片描述


1.2.4 IO多路转接

recv,read,recvfrom等接口一次只能对一个fd进行等待,为此,os提供了新的系统调用select,poll,epoll来解决这一问题,这些接口专门负责等待数据到来,而且可以同时监听多个fd.

从流程上看起来与阻塞IO类似,但是核心效率提升在于 IO多路转接能够同时等待多个文件描述符的就绪状态。

对于一个进程来说,虽然在某一时刻内只能处理一个请求,但是处理每个请求的事件时,耗时控制在1ms之内,这样1s内可以处理上千的请求,所以也叫做分时多路复用。

实现多路转接 可以使用内核提供给用户的接口:select,poll,epoll。
在这里插入图片描述

1.2.5 异步IO

内核在数据拷贝之后,通知应用程序。

这里要区分于信号驱动:

  • 信号驱动是告诉应用程序员何时可以开始拷贝数据。
  • 对于异步IO,调用后即返回,os全权托管,直接在数据到来后将数据拷贝到用户缓冲区。

在这里插入图片描述

1.3 同步通信 与 异步通信

同步和异步关注的是消息通信机制:

  1. 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得
    到返回值了; 换句话说,就是由调用者主动等待这个调用的结果
  2. 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.

五组 IO模型种,除了异步IO模型,其他都是同步IO.

注意这里的同步与异步 与 进程之间的同步,互斥没有任何关系。

1.4 阻塞 与 非阻塞

1.4.1 阻塞与非阻塞区别

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态

  1. 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回
  2. 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.

1.4.2 设置非阻塞IO

对于一个文件描述符,通常默认是阻塞IO,我们可以通过系统调用来设置 文件描述符的性质。

  • fcntl

在这里插入图片描述

传入的cmd的值不同,后面追加的参数列表也不同

fcntl函数有5种功能:

  1. 复制一个现有的fd (cmd = F_DUPFD)
  2. 获取/设置fd标记 (cmd = F_GETFD 或者 F_SETFD)
  3. 获取/设置文件状态标记 (cmd = F_GETFL 或者 F_SETFL)
  4. 获取/设置异步I/O所有权(cnd=F_GETOWN 或者 F_SETOWN)
  5. 获取/设置记录锁 (cmd=GETLK ,SETLK 或者 F_SETLKW)

想要将一个文件描述符设置为非阻塞的,只需要第三个功能。

之后的代码应用中,我会封装一个函数SetNonBlock函数来将文件描述符设置为非阻塞:

void SetNoBlock(int fd) {
	int fl = fcntl(fd, F_GETFL);
	if (fl < 0) {
		perror("fcntl");
		return;
	}
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

同学们可以跑一下下面的代码,会有什么现象?

#include <stdio.h>
#include<iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string>

void SetNoBlock(int fd) {
	int fl = fcntl(fd, F_GETFL);
	if (fl < 0) {
		perror("fcntl");
		return;
	}
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main() {
	SetNoBlock(0);

	while (1) {
		char buf[1024] = {0};
		ssize_t s = read(0, buf, sizeof(buf) - 1);
        if (s >0)
        {
            buf[s] = 0;
            std::cout<<"buffer: "<<buf<<std::endl;
        }
        else
        { 
            sleep(1);
            //EAGAIN = EWOUDBLOCK = 11
            if (errno == EAGAIN || errno == EWOULDBLOCK){
                std::cout<<"读取未出错,数据未就绪..."<<std::endl;
                continue;
            }
            else if(errno == EINTR){
                std::cout<<"读取被信号中断"<<std::endl;
            }
            else{
                std::cout<<"读取出错: "<<s<<std::endl;
                break;
            }
        }
	}
	return 0;
}

2. select

2.1 接口使用

系统提供select函数来实现多路复用输入/输出模型.

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

函数原型:
在这里插入图片描述
参数:

  1. nfds: 需要监视的最大文件描述符值 + 1.
  2. rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合,是输入输出型参数
  3. 参数timeout为结构timeval,用来设置select()的等待时间

先从最简单的timeout参数开始,其取值有三种:

  1. NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
  2. 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  3. 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

在这里插入图片描述

对于readfds,writefds,exceptfds,参数类型都是 fd_set*,那么 fd_set是什么呢?

在这里插入图片描述
在这里插入图片描述
可以发现,fd_set是一个结构体,里面有一个 固定大小的 位图, select使用位图种对应的位来表示要监视的文件描述符。

内核提供了一组操作 fd_set的系统接口,本质是方便用户对位图的增删查该:
在这里插入图片描述

  • void FD_CLR(int fd, fd_set *set); 用来清除描述词组set中相关fd 的位
  • int FD_ISSET(int fd, fd_set *set); 用来测试描述词组set中相关fd 的位是否为真
  • void FD_SET(int fd, fd_set *set); 用来设置描述词组set中相关fd的位
  • void FD_ZERO(fd_set *set); 用来清除描述词组set的全部位

  • 返回值
  1. 执行成功则返回文件描述词状态已改变的个数
  2. 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
  3. 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的
    值变成不可预测。

错误值可能为:
4. EBADF 文件描述词为无效的或该文件已关闭
5. EINTR 此调用被信号所中断
6. EINVAL 参数n 为负值。
7. ENOMEM 核心内存不足


2.2 select执行过程

select将已连接的socket都放入一个文件描述符集合之中,然后调用select函数将文件描述符集合拷贝到内核之中,让内核来检查是否有网络事件产生,检查的方式是通过线性遍历文件描述符集合的方式,当检查到有时间产生之后,将此socket标记为可读或者可写,接着将整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方式找到可读或者可写的socket,然后在对其进行处理。

我们这里以设置了读文件描述符集为例,来看一看readfds作为一个输入输出参数的意义是什么:

  1. 输入: 用户告诉内核,OS你要帮我检测一下再这个集合中的读事件。
  2. 输出:内核告诉用户,你关心的fd中,有哪些文件描述符就绪了(数据已就绪),可以读取。

在这里插入图片描述

由于输入输出型参数使用的是同一个变量,所以输出的时候参数会被覆盖,每次调用之后,我们需要重新设置一次。


2.3 select代码实践

下面是一段使用select实现的服务程序,为了简化结构,这只是一个单进程服务器,不过由于select的存在,一个进程也能够同时处理多个连接。

感兴趣的同学可以对这段代码进行升级改造。完整代码可以访问这里拿取:select_server

#include<iostream>

using namespace std;

#include "sock.hpp"
#include "sys/select.h"
#include "sys/time.h"
#include "sys/types.h"

namespace ns_select
{
#define NUM (sizeof(fd_set)*8)
    using namespace ns_sock;
    const int g_default = 8000;

    class SelectServer{
        private:
            u_int16_t port_;
            int listen_sock_;
            int fd_arrar_[NUM];

        public:
            SelectServer(int port = g_default)
            :port_ (port), listen_sock_(-1)
            {
                for( int i=0; i<NUM; i++)
                {
                    fd_arrar_[i] = -1;
                }
            }

            void InitSelectServer()
            {
                listen_sock_ = Sock::Socket();
                Sock::Bind(listen_sock_,port_);
                Sock::Listen(listen_sock_);
                fd_arrar_[0] = listen_sock_;
                cout<<"init select pool success,listen_sock="<<listen_sock_<<endl;
            }

            void  HandlerEvent(const fd_set &rfds)
            {
                for(int i=0;i<NUM;i++)
                {
                    if(fd_arrar_[i] == -1)
                        continue;
                    
                    if(FD_ISSET(fd_arrar_[i],&rfds))
                    {
                        if (fd_arrar_[i] == listen_sock_)
                        {
                            //new connect come
                            struct sockaddr_in peer;
                            socklen_t len = sizeof(peer);
                            int sock = accept(listen_sock_,(struct sockaddr*)&peer,&len);
                            if(sock <0)
                            {
                                std::cout<<"accept error"<<std::endl;
                            }
                            else
                            {
                                int j = 0;
                                for(;j<NUM;j++)
                                {
                                    if(fd_arrar_[j]==-1)
                                    {
                                        break;
                                    }
                                }

                                if(j == NUM){
                                    std::cout<<"fd array has full!"<<std::endl;
                                    close(sock);
                                }
                                else
                                {
                                    std::cout<<"get new connect success ,sock = "<<sock<<std::endl;
                                    fd_arrar_[j] = sock;
                                }
                            }
                        }
                        else
                        {   
                            char buffer[1024];
                            //new datas come
                            ssize_t s = recv(fd_arrar_[i],buffer,sizeof(buffer),0);
                            if(s > 0)
                            {
                                buffer[s] = '\0';
                                std::cout<<"client say# "<<buffer<<std::endl;
                            }
                            else if(s==0)
                            {
                                //conn closed
                                close(fd_arrar_[i]);
                                std::cout <<"client quit,sock = "<<fd_arrar_[i]<<std::endl;
                                fd_arrar_[i] = -1;
                            }
                            else
                            {
                                //read error
                                std::cerr<<"recv error"<<std::endl;
                            }
                        }
                    }
                }
            }

            void Loop()
            {
                //读事件位图
                fd_set rfds;
                //FD_SET(listen_sock_,&rfds);
                while(true){

                     FD_ZERO(&rfds); 
                     int max_fd_ = -1;
                     struct timeval timeout = {3,0};

                     for(int i=0;i<NUM;i++)
                     {
                        if(fd_arrar_[i]==-1)
                            continue;
                        //printf("fd_array[%d]: %d",i,fd_arrar_[i]);
                        FD_SET(fd_arrar_[i],&rfds);
                        if (max_fd_ < fd_arrar_[i])
                            max_fd_ = fd_arrar_[i];
                     }
                    //printf("max_fd: %d\n",max_fd_);
                    int n = select(max_fd_+1,&rfds,nullptr,nullptr,&timeout);
                    //printf("n: %d\n",n);

                    switch (n)
                    {
                    case 0:
                        std::cout<<"time out ..."<<std::endl;
                        break;
                    case -1:
                        std::cout <<"selct error"<<endl;
                        break;
                    default:
                        HandlerEvent(rfds);
                        std::cout<<"events coming"<<std::endl;
                        break;
                    }
                
                }
            }

            ~SelectServer()
            {
                if (listen_sock_>=0)
                    close(listen_sock_);
            }
    };
}

3. poll

3.1 接口使用

poll函数原型:
在这里插入图片描述
参数说明:

  1. fd是一个poll函数监听的结构列表,每一个元素之中,包含有三部分的内容:文件描述符,监听的事件集合,返回的事件集合。
  • pollfd的结构:
    在这里插入图片描述
  1. nfds 表示fds数组的长度
  2. timeout表示poll函数的超时时间(ms)

event 和 events 有以下取值:
在这里插入图片描述
在这里插入图片描述


  • 返回值

  • 返回值小于0, 表示出错;

  • 返回值等于0, 表示poll函数等待超时;

  • 返回值大于0, 表示poll由于监听的文件描述符就绪而返回.

3.2 poll的执行过程

poll不再使用BitsMap来存储所关注的文件描述符,而是使用动态数组,以链表的形式来组织,突破了select的文件描述符的个数限制。

其他过程与select的执行过程一致。

3.3 poll的优点与缺点

select 使用三个位图来表示三个 fdset,poll使用一个pollfd类型的指针解决。polldf结构使得每一个fd拥有event和revents,两者不需要再公用一个参数,因此调用更加方便。

虽然poll做出的优化,但是这个优化更像是方便了用户的理解和使用,再过程上与select 差不多。

  1. poll返回后,需要通过轮询pollfd来获取就绪的描述符
  2. 每次调用需要进行两次拷贝
  3. 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降

3.4 poll代码实践

poll代码与select写法基本一致,这里不再赘述,同学们可以自己尝试基于select进行修改即可。


4. epoll

按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.

4.1 接口使用

4.1.1 epoll_create

在这里插入图片描述
创建一个epoll的句柄

  • 自从linux2.6.8之后,size参数是被忽略
  • 用完之后,必须嗲用close()关闭

4.1.2 epoll_ctl

在这里插入图片描述
参数说明:

  1. 第一个参数是epoll_create()的返回值(epoll的句柄)
  2. 第二个参数表示要执行的操作,用宏来表示
  • EPOLL_CTL_ADD: 注册新的fd到epfd中
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件
  • EPOLL_CTL_DEL:从epfd中删除一个fd
  1. 第三个参数是需要监听的fd
  2. 第四个参数是告诉内核需要监听什么事件,其中epoll_event是一个结构体
    在这里插入图片描述
    events 可以是以下宏的组合:
  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
  • EPOLLERR : 表示对应的文件描述符发生错误;
  • EPOLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
  • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里

这里需要注意的一点是,有一种说法:Epoll使用内存映射机制,即内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销。
这样的表述是不准确的,原因在于epoll_event结构体作为一个输入输出参数,由用户手动创建,因此不可能优化到零拷贝,大概率是将用户数据拷贝到mmap映射区域,与内核数据关联。


4.1.3 epoll_wait

在这里插入图片描述
epoll的作用是收集epoll监控的事件中已经就绪的事件


4.2 epoll 的执行过程

对于select和epoll来说,都要求OS去主动检测特定的fd底层是否存在有效数据。
而epoll是通过在os中注册回调函数的回调机制。
在这里插入图片描述

4.3 epoll的优点

  1. 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  2. 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  3. 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
  4. 没有数量限制: 文件描述符数目无上限

4.4 epoll的两种工作方式

我们可以用下面的例子来快速了解EPOLL两种方式:

你正在吃鸡, 眼看进入了决赛圈, 你妈饭做好了, 喊你吃饭的时候有两种方式:

  1. 如果你妈喊你一次, 你没动, 那么你妈会继续喊你第二次, 第三次…(亲妈, 水平触发)
  2. 如果你妈喊你一次, 你没动, 你妈就不管你了(后妈, 边缘触发)

假设一个场景:

  1. 我们已经把一个tcp socket添加到epoll描述符
  2. 这个时候socket的另一端被写入了2KB的数据
  3. 调用epoll_wait,并且它会返回. 说明它已经准备好读取操作
  4. 然后调用read, 只读取了1KB的数据
  5. 继续调用epoll_wait…

4.4.1 水平触发(Level Triggered)

  • epoll默认状态下就是LT工作模式.

  • 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪.直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.

  • 支持阻塞读写和非阻塞读写

4.4.2 边缘触发(Edge Triggered)

  • 如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式
  • 当epoll检测到socket上事件就绪时, 必须立刻处理.如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候,epoll_wait 不会再返回了.也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
  • 只支持非阻塞的读写

4.4.3 对比LT和ET

LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.另一方面, ET 的代码复杂程度更高了

4.4.4 理解ET模式和非阻塞

使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 “工程实践” 上的要求.

假设这样的场景: 服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个10k请求.

如果服务端写的代码是阻塞式的read, 并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,(参考 man 手册的说明, 可能被信号打断), 剩下的9k数据就会待在缓冲区中

此时由于 epoll 是ET模式, 并不会认为文件描述符读就绪. epoll_wait 就不会再次返回. 剩下的 9k 数据会一直在缓冲区中. 直到下一次客户端再给服务器写数据. epoll_wait 才能返回

但是问题来了:服务器只读到1k个数据, 要10k读完才会给客户端返回响应数据=,客户端要读到服务器的响应, 才会发送下一个请求,客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据。

所以, 为了解决上述问题(阻塞read不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来。

此时,有同学会问,为啥非阻塞轮询就能全读出来,我使用阻塞轮询也可以全读出来。**原因在于应用层视角下,无法直接直到内核数据缓冲区的大小的,我们只能够通过尝试。**假设同步轮询下,前三次读取到数据,但是第四次把数据全读完了,但是我们并不知道,那么read会继续尝试读取,然后发现无数据而被挂起,也就是整个EPOLL进程被挂起。

但是,如果我们采用非阻塞轮询,当底层无数据时,OS会返回错误,对我们告知已经读完。

4.5 epoll代码实践

#pragma once

#include <iostream>
#include <string>
#include <sys/epoll.h>
#include "sock.hpp"

namespace ns_epoll
{
    using namespace ns_sock;
    const int g_port = 8000;

    class EpollServer
    {
    private:
        uint16_t port_;
        int listen_sock_;
        // epoll
        int epFd_;

    public:
        EpollServer(int port = g_port)
            : port_(port), listen_sock_(-1)
        {
        }
        void newEpollServer()
        {
            listen_sock_ = Sock::Socket();
            Sock::Bind(listen_sock_, port_);
            Sock::Listen(listen_sock_);
            epFd_ = epoll_create(128);
            if (epFd_ < 0)
            {
                std::cerr << "create epoll model error" << std::endl;
                return;
            }
        }

        void Loop()
        {
            struct epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = listen_sock_;
            // 将唯一的网络sock,listen_sock_加入epoll
            epoll_ctl(epFd_, EPOLL_CTL_ADD, listen_sock_, &ev);

#define EV_NUM 10
            struct epoll_event revs[EV_NUM];
            int timeout = 1000;

            while (true)
            {
                int num = epoll_wait(epFd_, revs, EV_NUM, 1000);
                switch (num)
                {
                case 0:
                    std::cout << "timeout..." << std::endl;
                    break;
                case -1:
                    std::cerr << "epoll error" << std::endl;
                    break;
                default:
                    HandlerEvent(revs, num);
                    break;
                }
            }
        }

        void HandlerEvent(struct epoll_event *revs, int num)
        {
            for (size_t i = 0; i < num; i++)
            {
                int sock = revs[i].data.fd;
                uint32_t event = revs[i].events;

                // 读事件就绪 (新连接到来 or 可读事件)
                if (event & EPOLLIN)
                {
                    // 监听sock事件就绪,还是普通读事件就绪
                    if (sock == listen_sock_)
                    {
                        struct sockaddr_in peer;
                        socklen_t len = sizeof(peer);
                        int sfd = accept(sock, (struct sockaddr *)&peer, &len);
                        if (sfd < 0)
                        {
                            std::cerr << "accept new connect error" << std::endl;
                            continue;
                        }
                        struct epoll_event ev;
                        ev.events = EPOLLIN;
                        ev.data.fd = sfd;
                        // select or poll需要我们将新增fd添加到数组中,也就是
                        // 手动管理fd,在epoll中变成epoll自动管理
                        epoll_ctl(epFd_, EPOLL_CTL_ADD, sfd, &ev);

                        std::cout << "new connect ,fd: " << sfd << std::endl;
                    }
                    else
                    {
                        char buffer[1024];
                        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
                        if (s > 0)
                        {
                            // 读取完毕
                            buffer[s] = 0;
                            std::cout << "client say: " << buffer << std::endl;

                            // 无法保证写入条件就绪,直接send,write可能阻塞
                            struct epoll_event ev;
                            ev.data.fd = sock;
                            ev.events = EPOLLIN | EPOLLOUT;
                            epoll_ctl(epFd_, EPOLL_CTL_MOD, sock, &ev);
                        }
                        else if (s == 0)
                        {
                            // 对端关闭
                            std::cout << "client quit,sock: %d" << sock << std::endl;
                            close(sock);
                            epoll_ctl(epFd_, EPOLL_CTL_DEL, sock, nullptr);
                        }
                        else
                        {
                            close(sock);
                            epoll_ctl(epFd_, EPOLL_CTL_DEL, sock, nullptr);
                        }
                    }
                }
                // 写事件就绪
                else if (event & EPOLLOUT)
                {
                    std::string messages = "ok";
                    send(sock, messages.c_str(), messages.size(), 0);

                    // 取消对于写事件的关心,读取事件结束,改为只读
                    struct epoll_event ev;
                    ev.data.fd = sock;
                    ev.events = EPOLLIN;
                    epoll_ctl(epFd_, EPOLL_CTL_MOD, sock, &ev);
                }
                // 其他事件处理...
            }
        }

        ~EpollServer()
        {
            if (listen_sock_ >= 0)
                close(listen_sock_);
            if (epFd_ >= 0)
                close(epFd_);
        }
    };
}

在下一篇文章,《多路转接(二) 》中,我们将继续深入epoll,探索基于epoll的更复杂的服务器设计模式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1419365.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ 数论相关题目:卡特兰数应用、快速幂求组合数。满足条件的01序列

给定 n 个 0 和 n 个 1 &#xff0c;它们将按照某种顺序排成长度为 2n 的序列&#xff0c;求它们能排列成的所有序列中&#xff0c;能够满足任意前缀序列中 0 的个数都不少于 1 的个数的序列有多少个。 输出的答案对 1097 取模。 输入格式 共一行&#xff0c;包含整数 n 。 …

开源大规模分布式MQTT消息服务器EMQX部署教程

1.EMQX是什么&#xff1f; EMQX 是一款开源的大规模分布式 MQTT 消息服务器&#xff0c;功能丰富&#xff0c;专为物联网和实时通信应用而设计。EMQX 5.0 单集群支持 MQTT 并发连接数高达 1 亿条&#xff0c;单服务器的传输与处理吞吐量可达每秒百万级 MQTT 消息&#xff0c;并…

数据结构----链表介绍、模拟实现链表、链表的使用

文章目录 1. ArrayList存在的问题2. 链表定义2.1 链表的概念及结构2.2 链表的组合类型 3. 链表的实现3.1 单向、不带头、非循环链表的实现3.2 双向、不带头节点、非循环链表的实现 4.LinkedList的使用4.1 什么是LinkedList4.2 LinkedList的使用4.2.1. LinkedList的构造4.2.2. L…

R语言(数据导入,清洗,可视化,特征工程,建模)

记录一下痛失的超级轻松的数据分析实习&#xff08;线上&#xff09;&#xff0c;hr问我有没有相关经历&#xff0c;我说我会用jupyter book进行数据导入&#xff0c;清洗&#xff0c;可视化&#xff0c;特征工程&#xff0c;建模&#xff0c;python学和用的比较多&#xff0c;…

burp靶场--xss上篇【1-15】

burp靶场–xss https://portswigger.net/web-security/cross-site-scripting 1. 什么是xss: 跨站脚本 (XSS) 是一种通常出现在 Web 应用程序中的计算机安全漏洞。XSS 允许攻击者将恶意代码注入网站&#xff0c;然后在访问该网站的任何人的浏览器中执行该代码。这可能允许攻击…

【重磅发布】已开放!模型师入驻、转格式再升级、3D展示框架全新玩法…

1月23日&#xff0c;老子云正式发布全新版本。此次新版本包含多板块功能上线和升级&#xff0c;为用户带来了含模型师入驻、三维格式在线转换升级、模型免费增值权益开放、全新3D展示框架等一系列精彩内容&#xff01; 1月23日&#xff0c;老子云正式发布全新版本。此次新版本…

【开源】基于JAVA语言的班级考勤管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统基础支持模块2.2 班级学生教师支持模块2.3 考勤签到管理2.4 学生请假管理 三、系统设计3.1 功能设计3.1.1 系统基础支持模块3.1.2 班级学生教师档案模块3.1.3 考勤签到管理模块3.1.4 学生请假管理模块 3.2 数据库设…

PyTorch自动微分机制的详细介绍

PyTorch深度学习框架的官方文档确实提供了丰富的信息来阐述其内部自动微分机制。在PyTorch中&#xff0c;张量&#xff08;Tensor&#xff09;和计算图&#xff08;Computation Graph&#xff09;的设计与实现使得整个系统能够支持动态的、高效的自动求导过程。 具体来说&#…

BL808学习日志-3-DPI-RGB屏幕使用-LVGL D0

一、DPI-RGB驱动 BL808的手册上显示是支持RGB565屏幕显示输出的&#xff0c;但是一直没找到网上的使用例程。且官方的SDK显示也是能够使用的&#xff0c;只是缺少了驱动。这一部分驱动在SIPEED的SDK中已经内置了&#xff0c;今天就是简单的点亮一个800*480 RGB565的屏幕。 二、…

第十一篇【传奇开心果系列】BeeWare的Toga开发移动应用示例:Briefcase和Toga 哥俩好

传奇开心果博文系列 系列博文目录BeeWare的Toga开发移动应用示例系列博文目录一、前言二、Briefcase和toga各自的主要功能分别介绍三、使用Toga 开发移动应用Briefcase工具是最佳拍档四、Briefcase搭档Toga创建打包发布联系人移动应用示例代码五、运行测试打包发布六、归纳总结…

OpenHarmony—ArkTS限制throw语句中表达式的类型

规则&#xff1a;arkts-limited-throw 级别&#xff1a;错误 ArkTS只支持抛出Error类或其派生类的实例。禁止抛出其他类型&#xff08;例如number或string&#xff09;的数据。 TypeScript throw 4; throw ; throw new Error();ArkTS throw new Error();限制省略函数返回类…

Codeforces Round 799 (Div. 4)

目录 A. Marathon B. All Distinct C. Where’s the Bishop? D. The Clock E. Binary Deque F. 3SUM G. 2^Sort H. Gambling A. Marathon 直接模拟 void solve() {int ans0;for(int i1;i<4;i) {cin>>a[i];if(i>1&&a[i]>a[1]) ans;}cout<&l…

欧拉角及Eigen库中eulerAngles函数的理解

欧拉角方向 以右手坐标系为例&#xff0c;大拇指表示X轴&#xff0c;食指表示Y轴&#xff0c;中指表示Z轴。 大拇指朝向某个轴的正方向&#xff0c;手掌弯曲的方向即为某个轴欧拉角的正方向。 Eigen库中eulerAngles函数 旋转矩阵转欧拉角(Z-Y-X&#xff0c;即RPY&#xff09…

防御保护----防火墙基本知识

一.防火墙的基本知识--------------------------------------------------------- 防火墙&#xff1a;可以想象为古代每个城市的城墙&#xff0c;用来防守敌军的攻击。墙&#xff0c;始于防&#xff0c;忠于守。从古至今&#xff0c;墙予人以安全之意。 防火墙的主要职责在于&…

IDE开发工具Idea使用(IDEA安装与卸载,详细配置,快捷键,代码模板,创建模板,Debug调试,生成javadoc,导入模块,导出jar)

文章目录 一、IntelliJ IDEA 介绍1、JetBrains 公司介绍2、IntelliJ IDEA 介绍3、IDEA 的下载 二、安装与卸载1、安装前的准备2、安装过程3、卸载过程方式一&#xff1a;【控制面板】中卸载如何打开控制面板&#xff1f; 三、初始化配置与激活四、HelloWorld1、新建Java类2、编…

Linux浅学笔记03

目录 有关root的命令 用户和用户组 用户组管理&#xff1a;&#xff08;以下需要root用户执行&#xff09; 创建用户组: 删除用户组&#xff1a; 用户管理&#xff1a;&#xff08;以下需要root用户执行&#xff09; 创建用户&#xff1a; 删除用户&#xff1a; 查看用…

开关电源调试会遇到哪些问题?怎么解决?

一般在使用电气设备之前都会调试&#xff0c;以便及时发现问题并采取措施解决。开关电源也一样会进行调试&#xff0c;那么在调试开关电源的过程中会遇到哪些问题呢? 又该如何解决呢? 1. 空载、轻载无法启动 开关电源在空载和轻载情况下&#xff0c;由于绕组的感应电压太低&a…

时隔3年 | 微软 | Windows Server 2025 重磅发布

最新功能 以下是微软产品团队正在努力的方向&#xff1a; Windows Server 2025 为所有人提供的热补丁下一代 AD 活动目录和 SMB数据与存储Hyper-V 和人工智能还有更多… Ignite 发布视频 Windows Server 2025 Ignite Video 介绍 Windows Server 2022 正式发布日期是2021年…

深度强化学习(王树森)笔记09

深度强化学习&#xff08;DRL&#xff09; 本文是学习笔记&#xff0c;如有侵权&#xff0c;请联系删除。本文在ChatGPT辅助下完成。 参考链接 Deep Reinforcement Learning官方链接&#xff1a;https://github.com/wangshusen/DRL 源代码链接&#xff1a;https://github.c…

网络防御安全知识(第二版)

安全策略 传统的包过滤防火墙 --- 其本质为ACL列表&#xff0c;根据数据报中的特征进行过滤&#xff0c;之后对比规制&#xff0c; 执行动作。 五元组 --- 源IP&#xff0c; 目标IP&#xff0c;源端口&#xff0c; 目标端口&#xff0c;协议 安全策略 --- 相较于ACL的改进之…