【Linux】select/poll/epoll/reactor 附代码详解

文章目录

  • IO的概念
  • 高级IO为何高效
  • 五种IO模型
  • fcntl
  • select
    • select代码测试
    • select总结
  • poll
    • poll代码测试
    • poll总结
  • epoll
    • epoll模型
    • 重新理解三大接口
    • epoll测试1
    • epoll的工作模式
    • epoll总结和零碎细节
  • Reactor
    • epoll测试2
  • 理解与总结


IO的概念

【Linux】select/poll/epoll/reactor 附代码详解_第1张图片
操作系统如何知道网卡当中有数据:
两种方案:
第一种是由操作系统定期轮询;第二种:当中数据到来时通过驱动提醒操作系统
我们计算机是通过中断程序来做到的,8259,中断组件来实现的。等外设来的时候,会像CPU直接发消息(数据不会,但是控制信号可以直接通知CPU)。CPU通过中断向量表,通过其中的方法进行对应的操作。而当网卡有数据,就是通知CPU去将数据从网卡搬运到内存当中。

底层数据到达时操作系统做了啥:
比如底层网卡有数据到达,此时硬中断通知操作系统,操纵系统生成软中断对数据进行拷贝工作。

中断的使用场景:
通常引入中断都是外设,系统当中数据准备就绪,需要拷贝到内存,需要硬件层面上,需要中断来完成。

waitpid使用了中断吗?
没有,因为本身是软件,父子进程有连接关系,子进程退出,根据pcb之间数据关系找到父进程。



高级IO为何高效


谈高级IO为何高效前,我想先说说为何只调用Read,Write为何低效

在之前的网络套接字编写,我们调用write,read写入,或者读取时,我们进程进行阻塞,此时称之为IO,尤其时套接字场景,读取数据的时候不知道会被阻塞多久,此时由于我们只是等待一个文件描述符,所以比较低效。
【Linux】select/poll/epoll/reactor 附代码详解_第2张图片


高效IO的本质:
因为调用select,poll,epoll进行等待的时候可以等待批量文件描述符,等待的事件重叠了,自然就高效了。


下面会具体讲述这几种IO方式,首先先来了解IO模型究竟有哪些



五种IO模型


IO模型光用图讲述没有意思,我们这里用一个例子将五种IO模型讲通!!

生动例子解释:
现在我们将钓鱼简化,过程可以分为等,钓两个步骤。
钓鱼佬们如何提高效率钓到更多的鱼呢?

看看下面五个人的做法,谁有可能钓到更多的鱼
张三:阻塞式钓鱼
李四:一边玩手机,时不时看看鱼鳔。
王五:鱼竿带铃铛,忙自己事情,等铃铛响后再钓鱼。
赵六:一大堆鱼竿,在岸边进行轮询检测,只要有一个鱼咬钩就可以钓了。
田七:派人去钓鱼,钓到了给田七打电话,钓鱼的桶相当于缓冲区。

答案:赵六(田七先不考虑)

  • 其中张三的做法是阻塞,类似之前我们进程在调用recv,flags设置成0,直接阻塞式等待,进程放入文件描述符中的等待队列,状态被设置为!R,直到底层有数据,且高于低水位线,操作系统才将进程设置为R状态,放入运行队列。
  • 李四的做法是非阻塞式的检测,这种做法对CPU的消耗会比较高。
  • 王五的做法式信号驱动IO,当有IO来的时候会给进程发送(29) SIGIO,这种做法效率很高,王五只需要做自己的事情,等到信号来了再钓鱼,但是由于29号是普通信号,可能信号发送多次而王五只处理了一次。所以只在小型的通信中可能会使用。
  • 赵六:多路转接式钓鱼,每个鱼竿有反应的概率相同的情况,增加鱼竿的数目能让赵六将等待的时间重叠起来,是一种非常高效的方式(select,poll)。
  • 田七:异步IO的思想,让人帮忙钓鱼,不关心这个人怎么把鱼钓上。但是帮忙钓鱼的人可以采用前面那些人的方式。

高效IO的本质:
而在等待过程特别长的通信过程中,让等的时间占比特别高,此时IO大部分时间都在等待,效率就会非常低下;高效IO,在一次IO中让等待的比重小。recv读不是立马有数据就读的,还要等数据超过低水位线,或者等对端给我发送PSH字段才会通知上层将数据从内核拷贝数据到用户。

如何使用信号驱动IO

操作系统收到数据,就会像进程发送SIGIO,默认处理动作是忽略,我们可以注册SIGIO的处理方法,当底层好了给我发信号,我再调用read接受就可以。简单IO用的多,复杂IO用的少,因为他是普通信号,只会记录一个信号,信号有可能会丢失。

同步IOvs异步IO
同步与异步最大的差别在于数据拷贝的过程,异步式IO不关心数据拷贝,只提供一块缓冲区,让OS在合适的时候拷贝到缓冲区当中。即拷贝的过程由操作系统完成! 数据就绪的通知方案是由信号所决定的。
类似家中来了客人,母亲在做菜,此时我是负责端菜的,我可以以同步IO当中的方式进行端菜,但对于客人来说,不需要帮忙,对于客人来说就是异步式的。

一句话总结:同步IO需要用户调用recv/read将数据拷贝到缓冲区,而异步IO需要用户提前告知缓冲区,操作系统会挑选合适时机拷贝数据。

为什么是内核收到数据:
因为协议栈是自底向上收的,所以是操作系统先收到,硬件,驱动,操作系统,用户。这个问题虽然简单,但是很关键,通常是由我们的硬件设备网卡检测到有数据,然后通过中断拷贝至系统的缓冲区。

复盘多路转接高效的原因:
如select,select的时候阻塞式等待多个文件描述符的事件就绪,只要有一个准备好了,就会调用recv将数据读取上来。recv一次只能等一个,因为他的参数只有一个,但是select后能保证recv不会被阻塞住。 select只负责等的环节,后续recv就不会被阻塞,因为数据已经就绪。
但是细节有很多,比如数据就绪我能够疯狂读取吗?或者我能读取一部分就不读了吗,下次他还会送来给我读取吗?(这些在epoll模式当中讲解)

【Linux】select/poll/epoll/reactor 附代码详解_第3张图片

阻塞vs非阻塞(这里先简单理解,后续有加深理解)

  • 单进程阻塞接口直观看到进程卡住了,再等待某个事件就绪。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.

在真正讲多路转接的接口前还需要介绍一个函数,因为这个接口在后面真正编码的时候起很大的作用。

fcntl


一个文件描述符, 默认都是阻塞IO,fcntl可以让文件描述符为非阻塞的
但是除了fcntl设置,还有很多方法,如open的时候设置第二个参数为O_NONBLOCK,可以让打开的文件描述符就是非阻塞的。或者在调用recv等接口的时候设置flags位置为O_NONBLOCK。

函数介绍

#include
#include
int fcntl(int fd, int cmd, … /* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同.
fcntl函数有5种功能:
复制一个现有的描述符(cmd=F_DUPFD).
获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.

接口测试

测试代码:默认read读取为非阻塞,每次往标准输入读取一个数据,默认情况缓冲区为空,便会卡住等待我们输入。

#include
#include


int main()
{
  //观察标准输入阻塞和非阻塞状态读取数据
  char ch;
  while(1)
  {
    //每隔一秒向标准输入拿数据
    sleep(1);
    ssize_t s  = read(0,&ch,1);
    if(s > 0)
    {
      //读取成功
      printf("%c\n",ch);
    }
    else{
      printf("error :%d\n",s);
    }
    printf("...........................\n");//标识一次读取完成
  }
  return 0;
}

结果:当我们不输入的时候,卡住等待我们输入。
【Linux】select/poll/epoll/reactor 附代码详解_第4张图片

非阻塞式:
代码:

#include
#include
#include
void SetNonBlock(int fd)
{
  //获取之前文件的状态
  int fl = fcntl(fd,F_GETFD);
  if(fl < 0)
    perror("fl");

  //将fd文件描述符添加一个O_NONBLOCK,即fl|O_NONBLOCK
  fcntl(fd,F_SETFL,fl|O_NONBLOCK);
}
int main()
{
  //观察标准输入阻塞和非阻塞状态读取数据
  char ch;
  //设置0为非阻塞文件描述符
  SetNonBlock(0);

  while(1)
  {
    //每隔一秒向标准输入拿数据
    sleep(1);
    ssize_t s  = read(0,&ch,1);
    if(s > 0)
    {
      //读取成功
      printf("%c\n",ch);
    }
    else{
      printf("error :%d\n",s);
    }
    printf("...........................\n");//标识一次读取完成
  }
  return 0;
}

结果:

设置文件描述符为非阻塞,当缓冲区没有数据,read失败直接返回。ssize_t是一个有符号整数。-1代表底层数据没有就绪,读取数据不算错误,而是一种通知。并且会设置errno为EAGAIN,代表try again,表示底层没有数据准备好,下次再来;EWOULDBLOCK 也是相同效果,在某些平台他们的值是相同的,但是这里这样写是为了能够适应不同平台。
若错误码为EINTR,表示阻塞等待的时候被信号中断。
【Linux】select/poll/epoll/reactor 附代码详解_第5张图片

所以再改一改代码:
测试:

#include
#include
#include
#include
void SetNonBlock(int fd)
{
  //获取之前文件的状态
  int fl = fcntl(fd,F_GETFD);
  if(fl < 0)
    perror("fl");

  //将fd文件描述符添加一个O_NONBLOCK,即fl|O_NONBLOCK
  fcntl(fd,F_SETFL,fl|O_NONBLOCK);
}
int main()
{
  //观察标准输入阻塞和非阻塞状态读取数据
  char ch;
  //设置0为非阻塞文件描述符
  SetNonBlock(0);

  while(1)
  {
    //每隔一秒向标准输入拿数据
    sleep(1);
    ssize_t s  = read(0,&ch,1);
    if(s > 0)
    {
      //读取成功
      printf("%c\n",ch);
    }
    else if(s < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
    {
    //非阻塞读取的时候底层数据没有就绪
      printf("read condition fail!\n");
    }
    else if(s < 0 && errno == EINTR){
		//读取被信号中断
		continue;
	}
    else{
      printf("error :%d\n",s);
    }
    printf("...........................\n");//标识一次读取完成
  }
  return 0;
}

结果:
即errno为11我们可以单独设置一种模式,再去尝试即可,这里就是区分是读出错,还是读条件没有就绪。 非阻塞时通常什么时候读一次是由我们的业务决定的。
【Linux】select/poll/epoll/reactor 附代码详解_第6张图片


!!!!加油,看到这里的时候,已经将基础知识给干完了,接下来就是第一个难点。


select


select是一种就绪事件的通知方案

接口介绍

int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
【Linux】select/poll/epoll/reactor 附代码详解_第7张图片
【Linux】select/poll/epoll/reactor 附代码详解_第8张图片

参数解释:

  • 第一个参数nfds表示等的文件描述符最大值加1,即今天想等7个文件描述符,即1~7号,我们这个值填8。注意不是文件描述符的个数,内核会以前闭后开的方式去等。
  • 其中的fd_set是一个位图,是一个输入输出型参数,代表用户告诉内核需要关心哪些文件描述符,内核告诉用户则通知用户哪些事件已经就绪。
  • select提供读事件,写事件,异常事件三种事件的等待就绪通知;对应后面三个参数
  • timeval是秒和微妙的一个结构体,标识用户需要内核多少事件进行一个轮询检测。用户可以进行连接管理。
  • 返回值:大于0表示有文件描述符就绪,返回值的个数决定有几个就绪;如果返回值为0表示等待timeout时间内并没有文件描述符就绪;返回值是-1表示等待出现错误了。

timeout时间解释:

  • nullptr表示阻塞方式等待:
  • 若等待时间都是0,就是非阻塞方式,即没有就绪立马返回;
  • 若是!0,则!0时间以阻塞方式等待,时间到了0就返回一次。

使用select需要的注意点:

  • fd_set文件描述符集,可以设置批量的文件描述符进去,这个参数即是输入型参数,即每次进行select的时候都需要重复设置!!
  • readfs表示读事件是否有就绪,writefds表示写事件就绪。exceptfds表示哪一个文件描述符在读取的时候报错了。
  • readfds设置时输入代表用户告诉操作系统关心哪些文件描述符读事件,fd_set是一张位图,和信号的sigset_t一样,比特位的位置表示哪一个文件描述符,比特位的内容:输入表示是否需要关心特定文件描述符的读就绪事件,输出表示内核告诉用户某些文件描述符的读事件已经就绪。
  • 默认操作系统检测是以轮询的方案,只会轮询检测我们需设置进去的文件描述符
  • fd_set可以理解成不具有保存能力,只负责在用户和内核之间相互通知;所以用户需要自定义缓冲区保存需要IO的文件描述符。并且timeout时间也是如此。
  • timeout时间作为连接管理,返回值作为返回剩余的时间,一般可以用作建立最小堆,拿所有连接timeout时间最短的作为timeout时间,进来堆再调整堆。
  • 链接请求也是当读事件处理的,并且是以读事件告知给操作系统的;所以listen 套接字也需要被添加到select的readfds当中,监听套接字上只有读事件。
  • select需要一个辅助数组,把已经打开的文件描述符保存起来。若既要读又要写,则需要两个数组。

fd_set中比特位的意义,以readfds为例子,其他同理:

【Linux】select/poll/epoll/reactor 附代码详解_第9张图片
操作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的全部位

编码提醒:
select和poll相同,在水平模式下不需要将文件描述符设置为非阻塞形式。
当要处理http协议,空行之前不能处理请求,没有读够就暂时对这个不做处理,下次读够了再进行处理。每个文件描述符应该要有一个缓冲区。就要建立fd和request的映射,request里面buffer里面。

select代码测试

Server.cc

#include"Sock.hpp"
#include"Server.hpp"

int main(int argc,char* argv[])
{
  cout << sizeof(fd_set)*8 <<endl;
  if(argc != 2)
  {
    exit(1);
  }

  Server* svr = new Server(atoi(argv[1]));
  svr->InitServer();
  svr->Start();
  return 0;
}

Sock.hpp

#pragma once
#include
using namespace std;
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class Sock
{
  public:
    static int  Socket()
    {
      int sock = socket(AF_INET,SOCK_STREAM,0);
      if(sock < 0)
      {
        perror("sock");
        exit(2);
      }
      return sock;
    }

    static void Bind(int sockfd,int port)
    {
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;
      addr.sin_addr.s_addr = INADDR_ANY; 
      addr.sin_port = htons(port);

      
      socklen_t len = sizeof(addr);
      if(bind(sockfd,(struct sockaddr*)&(addr),len) < 0)
      {
        perror("bind");
        exit(3);
      }
    }

    static void Listen(int sockfd)
    {
     if(listen(sockfd,5) < 0)
     {
       perror("Listen");
       exit(4);
     }
    }

    static int Accept(int sockfd)
    {
     struct sockaddr_in peer;
     socklen_t len = sizeof(peer);
     int fd = accept(sockfd,(struct sockaddr*)&peer,(socklen_t*)&len); 
     if( fd < 0 )
     {
       perror("accept");
     }
     return fd;
    }
    static void Setsockopt(int sockfd)
    {
      int opt = 1;
      setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    }

};

Server.hpp

#pragma once
#include "Sock.hpp"


#define NUM (sizeof(fd_set)*8) 
//#define NUM  1000 
#define DEL_NUM -1

class Server
{
  private:
    int lsock;
    int port;
    int rfds_array[NUM];
  public:
    Server(int p = 8080):lsock(-1),port(p)
    {}

    void InitServer()
    {
      for(int i = 1; i < NUM ;++i)
      {
        //将默认值设置为DEL_NUM
        rfds_array[i] = DEL_NUM;
      }

      lsock = Sock::Socket();
      Sock::Setsockopt(lsock);
      Sock::Bind(lsock,port);
      Sock::Listen(lsock);
      //初始化rfds_array[0]为lsock,负责接受链接
      rfds_array[0] = lsock;
    }

    void DelFd(int index)
    {
     rfds_array[index] = DEL_NUM; 
    }

    void Add2RSock(int fd)
    {
      int i =0; 
      
      for(; i < NUM; ++i)
      {
        if(rfds_array[i] == DEL_NUM)
        {
          break;
        }
      }
      if(i >= NUM)
      {
        //链接数到达上线,无法处理链接
        printf("link num reach the total num\n");
      }
      else 
      {
        cout << "new link index: "<<i <<endl;
        sleep(1);
        rfds_array[i] = fd;
      }

    }
    void HandlerEvents(fd_set* rfds)
    {
      for(int i = 0; i < NUM;++i)
      {//这里实际上只会对有关心的文件描述符做FD_ISSET检测。
        if(rfds_array[i] != DEL_NUM)
        {
          //从rfds_array当中判断关注哪些读文件描述符
          if(FD_ISSET(rfds_array[i],rfds))
          {
            //处理事件
            if(rfds_array[i] == lsock)
            {
              //新的链接就绪
              printf("new link ....\n");
              //新的链接上来需要加入rfds_array当中,
              //因为连接上服务器不一定马上发送数据
              struct sockaddr_in peer;
              socklen_t len = sizeof(peer);
              int fd = accept(lsock,(struct sockaddr*)&peer,&len);
              Add2RSock(fd);
            }
            else
            {
              //data readly
              //bug,读取数据应结合业务
              char buf[10240];
              ssize_t s =recv(rfds_array[i],buf,sizeof(buf)-1,0);
              if(s > 0)
              {
                //这里认为发送成功
                buf[s] =0 ;
                cout << "Server recv# "<<buf<<endl;
              }
              else if(s == 0)
              {
                //写端关闭,读端关闭
                close(rfds_array[i]);
                //从rfds_array当中拿出,无需在进行读等待
                DelFd(i);
              }
              else 
              {
                //读取发生错误,这里处理方式关闭文件描述符
                close(rfds_array[i]);
                //从rfds_array当中拿出,无需在进行读等待
                DelFd(i);
              }
            }
          }
        }
      }
    }

    void Start()
    {
      while(1)
      {
        //每次time_out都需要重新设置rfds,成本高。
        //是否返回值为0的时候不用走这里
        int maxfd = lsock;
        fd_set rfds;
        FD_ZERO(&rfds);
        printf("FDS_ARRAY#");
        fflush(stdout);
        //设置进fd_set哪些文件描述符需要读
        for(int i = 0; i < NUM;++i)
        {
          if(rfds_array[i] != DEL_NUM)
          {
            //rfds_array的下标无意义,值代表文件描述符
            FD_SET(rfds_array[i],&rfds);
            printf("%d ",rfds_array[i]);
            //跟新maxfd
            if(rfds_array[i] > maxfd)
              maxfd = rfds_array[i];
          }
        }
        cout << endl;
        //每隔5sselect不成功返回一次,注意在循环内部每次都需要重新定义
        struct timeval timeout = {5,0};
        switch(select(maxfd+1,&rfds,nullptr,nullptr,&timeout))
        {
          case -1:
            //失败
            perror("select error");
            break;
          case 0:
            //time_out返回,但无事件就绪
            printf("time_out ......\n");
            break;
          default:
            //有事件就绪
            printf("event readly\n");
            HandlerEvents(&rfds);
            break;
        }

      }
    }
};

【Linux】select/poll/epoll/reactor 附代码详解_第10张图片

select总结

缺点:

  • select,fd_set的大小是对应的,比特位的个数是确定的,文件描述符是有上限的。并且不能处理海量请求。
#include"Sock.hpp"
#include"Server.hpp"

int main(int argc,char* argv[])
{
  cout << sizeof(fd_set)*8 <<endl;
  return 0;
}

说明select服务器的上限在1024个左右,重新编译内核源代码可以让fd_set的长度变大。
在这里插入图片描述

select充满循环检测的过程,每次都要需要文件描述符的重新添加,且文件描述符增多,要添加的就要变多。并且云服务器上打开文件个数已经被重新设置,一个进程能打开的文件描述符有10万个。

【Linux】select/poll/epoll/reactor 附代码详解_第11张图片

  • select当中等待的文件描述符需要用户自己维护,并且每次调用select都需要重新把文件描述符集设置到内核的数据结构当中;
  • select当中等待额的文件描述符集合受到结构体fd_set的限制,在一般的平台通常只能等待上1024个文件描述符,而通常服务器会将进程等待的文件描述符设置成上十万个,这样子就没有充分利用服务器的资源。
  • select的返回值为多少个文件描述符就绪,上述代码只用来判断有和没有文件描述符就绪,通过遍历我们定义的数组知道哪些文件描述符就绪。
  • 每次调用select的时候,都需要频繁的进行上下文切换,数据拷贝。且select没有解决方案。



select就到此为止,最难的部分已经过去了!!!!

poll


poll函数声明:

#include
int poll(struct pollfd fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
int fd; /
file descriptor /
short events; /
requested events /
short revents; /
returned events */
};
【Linux】select/poll/epoll/reactor 附代码详解_第12张图片【Linux】select/poll/epoll/reactor 附代码详解_第13张图片

参数分析:

  • poll的第一个参数表示用户层定义需要关心文件描述符的最大多少,而nfds是告诉内核关心到多少,内核会从0遍历到nfds-1。
  • 第二个参数表示关心的文件描述符的数量。
  • timeout是毫秒,0表示非阻塞,-1为永久阻塞。并且不是输入输出型参数

events和revent的取值范围
两个最重要POLLIN/POLLOUT,当然events可以关心POLLIN和POLLOUT,可以一次性关心16种(如果有16种的话)。
POLLIN是针对进程来说的。
【Linux】select/poll/epoll/reactor 附代码详解_第14张图片

poll注意的点:

  • struct pollfd fds[nfds]是用户层定义,这里用户需要多少就可以定义多大;并且这个数组并不是一定要全部塞满,可以通过一些方法说明部分需要关心。
  • struct pollfd是输入输出型参数分隔,不需要如同select需要重复设置

最重要的两点之一:
man手册当中有这样一句话,当我们将fd设置为负数,内核就不会关心这个文件描述符,而是将revent设置为0返回。由于我们定义的struct pollfd 不是全部都使用,所以不使用的我们将fd 设置为负数即可,代码写的设置成-1;这个是用户通知内核需要关心哪些文件描述符的重要方法。

The field fd contains a file descriptor for an open file. If this field is negative, then the corresponding
events field is ignored and the revents field returns zero. (This provides an easy way of ignoring a file
descriptor for a single poll() call: simply negate the fd field.)

最重要的最后一点:
可以考虑当读时间处理完,处理revent为0,我们每次遍历的时候也看revent是否有事件就绪即可。
【Linux】select/poll/epoll/reactor 附代码详解_第15张图片



poll代码测试

Poll.cc
代码仅适合作为参考~

#include"socket.h"
#include
 #include 
#define NUM 10
using std::cout;
using std::cerr;
using std::endl;
namespace ns_poll
{
    class Poll 
    {
        private:
        int _lsock;
        int _port;
        string outbuffer;
        public:     
        Poll(int port):_port(port)
        {}

        void InitPollServer()
        {
            _lsock = Sock::Socket();
            if(_lsock < 0) 
            {
                std::cerr << "socket" << std::endl;
                exit(1);
            }
            Sock::Bind(_lsock,_port);
            Sock::Listen(_lsock);
        }

        void HandlerEvent(struct pollfd fds[NUM])
        {
            for(int i = 0;i < NUM;++i)
            {
                //设置为-1的文件描述符内核不会关心
                if(fds[i].fd != -1)
                {
                    //注意是revent
                    if(fds[i].revents & POLLIN)
                    {
                        //POLLIN : 监听套接字
                        if(fds[i].fd == _lsock)
                        {
                            struct sockaddr_in peer;
                            socklen_t len = sizeof(peer);
                            int fd = accept(_lsock,(struct sockaddr*)&peer,&len);
                            int j; 
                            for(j = 0;j < NUM;++j)
                            {
                                if(fds[j].fd == -1)
                                {
                                    break;
                                }
                            }
                            if(j == NUM)
                            {
                                cerr << "空间不足啦" << std::endl;
                                close(fd);//可以扩容,也可以关闭
                            }
                            else{
                                fds[j].fd = fd;
                                fds[j].events = POLLIN;
                            }
                        }
                        else
                        {
                            //POLLIN 第二种情况
                            char buffer[10240];
                            ssize_t s = recv(fds[i].fd,buffer,sizeof(buffer),0);
                            if(s > 0)
                            {
                                buffer[s] = 0;
                                cout << "client send: "<<buffer << endl;
                            }
                            else if(s == 0)
                            {
                                close(fds[i].fd);
                                fds[i].fd = -1;
                            }
                            else{
                                cerr << "recv" << endl;
                                exit(2);
                            }
                            outbuffer += buffer;
                            fds[i].events |= POLLOUT;
                            fds[i].revents ^= POLLIN;//就是将输出POLLIN取消
                        }
                    }
                    if(fds[i].revents & POLLOUT)
                    {
                        ssize_t s = write(fds[i].fd,outbuffer.c_str(),outbuffer.size());
                        cout << s << endl;
                        outbuffer = outbuffer.substr(s);
                        fds[i].revents ^= POLLOUT;//就是将输出POLLOUT取消
                        fds[i].events ^= POLLOUT;//也不要再输出了
                    }
                }
            }
        }
        void Loop()
        {
            #define NUM 10
            struct pollfd fds[NUM];
            //初始化,用户层定义默认fd为-1表示不需要关心
            for(int i = 0;i < NUM;++i)
            {
            // 内核定义当event字段为0则不关心。
                fds[i].fd = -1;
                fds[i].events= 0;
                fds[i].revents= 0;
            }
            int timeout = -1;
            //一开始需要关心lsock
            fds[0].fd = _lsock;
            fds[0].events = POLLIN;

            for(;;)
            {
                switch(poll(fds,NUM,timeout))
                {
                    case 0:
                    cout << "time out ..." << endl;
                    break;
                    case -1:
                    cerr << "poll" << std::endl;
                    exit(2);
                    break;
                    default:
                    HandlerEvent(fds);
                    break;
                }
            }
        }
    };
}
using namespace ns_poll;

int main()
{
    Poll p(8081);
    p.InitPollServer();
    p.Loop();
    return 0;
}

服务器端:
【Linux】select/poll/epoll/reactor 附代码详解_第16张图片

客户端:
【Linux】select/poll/epoll/reactor 附代码详解_第17张图片

poll总结

优点:

  • 对比select,poll等的文件描述符没有上限,并且不需要每次对文件描述符进行添加;
  • 对比起select,不需要每次重复设置哪些事件需要关心。

缺点:

  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.因为poll只有一个接口,操作系统需要知道用户做了哪些文件描述符的哪些关心事件的修改需要遍历整个数组,而epoll有epoll_ctl,只有在一开始需要设置,后续更改一个节点的关心事件是O(1),而poll是O(N)。
  • 并且poll获得哪些事件就绪,即用revents|POLLIN,revents|POLLOUT等,对于每个添加的文件描述符都需要做这个操作,时间复杂度O(N),epoll只需要遍历就绪队列的节点,时间复杂度为O(K),K为就绪队列的节点,并且每一个节点都是有用的!!!
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.



看完上面,就到了最终版本epoll,离成功只差一点点!!

epoll

epoll是就绪事件通知方案
句柄:唯一标定某种系统资源的标识符。
如文件描述符,能标识某个文件,所以称之为文件句柄。

epoll函数声明

int epoll_create(int size);//创建epoll模型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//修改底层的红黑树
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);//检测底层的就绪队列是否有数据

epoll_create使用细节:
epoll_create的参数不需要,传什么都可以。只是为了新老版本的兼容。返回值是一个文件描述符。OS帮助我们创建一个epoll模型。内核层创建的。

epoll_ctl使用细节:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
第一个参数是epoll_create()的返回值(epoll的句柄).
第二个参数表示动作,用三个宏来表示.
第三个参数是需要监听的fd.
第四个参数是告诉内核需要监听什么事.

【Linux】select/poll/epoll/reactor 附代码详解_第18张图片

op的选项:
都是从epoll模型当中的红黑树做操作,都是用户->内核

EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;

epoll_event的价值最大的是events,events的取值和poll一样,只是名字更改了,下面的epoll_data_t是一个联合体,也可以用它来封装一些其他的信息(用户的可用数据,如下面可以将void* ptr存放入一些信息)。

epoll_wait:负责等待,从底层就绪队列获取事件。

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

第一个参数是epoll模型对应的文件描述符,第二个参数和第三个参数是输出型参数,由操作系统->用户哪些文件描述符的事件已经就绪。
第二个参数是输出型参数,定义了直接传进去即可。
第三个参数告诉OS第二个参数定义的缓冲区有多大。
其中timeout的作用和poll一摸一样。

其中底层就绪队列的数量可以大于我们的maxevents,因为底层是做就绪事件管理的,OS会将部分拷入到我们的就绪队列。

并且返回值可以告诉有几个就绪,就绪的会依次放在events里面依次存放。直接for()遍历完返回值就可以了!!这里的返回值就是有意义的。

注意:

  • epoll_create创建epoll模型,用完后返回一个文件描述符,使用完需要close关闭掉。
  • 定义的数组的意义不同了。用户要定义一个数组,但这个数组对比poll,它并不作为一个管理就绪文件描述符的工作,而是对存放每次的事件就绪的文件描述符,并且返回值就是事件就绪的个数。所以当我们使用这个数组,每次都是有效的操作。
  • 要关心的文件描述符,就绪的文件描述符都有内核维护,底层通知上层的时候只需要提供缓冲区供给OS自动会拷贝进来;保证了O(1)检测事件是否就绪;O(N)获取全部就绪事件。

epoll模型

【Linux】select/poll/epoll/reactor 附代码详解_第19张图片
epoll模型:

  • 一颗红黑树,节点上面主要关心文件描述符fd和文件描述符对应的事件events;
  • 注册网卡驱动的回调函数,当事件就绪会调用网卡驱动方法,主要工作就是创建就绪队列的节点,告知就绪队列节点新增一个;
  • 就绪队列,队列存放的用户需要内核关心的事件。
  • 其中红黑树,就绪队列,回调机制构成epoll模型

红黑树的键值是文件描述符,天然不会重复。

重新理解三大接口

epoll_create:

  • 创建红黑树,创建就绪队列,与进程的文件描述符联系;
  • 而epoll_create是进程调用的,进程通过file_struct里面的fd_array里面存放的一个struct file*指针其实就可以指向一个数据结构的起始。

epoll_ctl

  • 在红黑树新增,修改,和删除。假如现在我们需要让系统关心1,2,3的读事件,系统是不会给4号文件描述符建立回调机制的。
  • 创建/消除对应文件描述符的回调机制。
    创建节点会在OS底层文件描述符底层的回调机制。即有事件就绪让OS生成一个节点(revents)放到就绪队列

epoll_wait

  • 检测就绪队列是否有节点即可。检测的效率高(O(1)),即使有大批量文件描述符就绪,效率依旧很高。

内核的数据结构一览:

 //epitem是红黑树节点,里面最重要的字段是fd,,rdlist就是就绪队列。
 struct eventpoll {
	/* Protect the this structure access */
	spinlock_t lock;

	/*
	 * This mutex is used to ensure that files are not removed
	 * while epoll is using them. This is held during the event
	 * collection loop, the file cleanup path, the epoll file exit
	 * code and the ctl operations.
	 */
	struct mutex mtx;

	/* Wait queue used by sys_epoll_wait() */
	wait_queue_head_t wq;

	/* Wait queue used by file->poll() */
	wait_queue_head_t poll_wait;

	/* List of ready file descriptors */
	/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/ 
	struct list_head rdllist;

	/* RB tree root used to store monitored fd structs */
	 /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ 
	struct rb_root rbr;

	/*
	 * This is a single linked list that chains all the "struct epitem" that
	 * happened while transfering ready events to userspace w/out
	 * holding ->lock.
	 */
	struct epitem *ovflist;

	/* The user that created the eventpoll descriptor */
	struct user_struct *user;
};

epoll测试1

这里我们会使用epoll_data_t 当中的void* ptr。

struct bucket
{
char str[10240];
int fd;
}

编码注意:

  • 一个文件描述符,需要读和写的缓冲区;
  • 我们自己定义协议,当检测到data里面ptr为空,就是监听套接字。

Sock.hpp用的是前面一个案例的

#include"Sock.hpp"

struct Bucket
{
  int fd;
  char buf[20];//协议
  int pos;
  Bucket(int _f):fd(_f),pos(0)
  {}
};

class Server
{
  private:
    int lsock;
    int epollfd;//epoll模型
    int port;
  public:
    Server(int p = 8080)
      :lsock(-1),epollfd(-1),port(p)
    {}
    ~Server()
    {
      close(lsock);
    }
    void InitServer()
    {
      lsock = Sock::Socket();
      Sock::Setsockopt(lsock);
      Sock::Bind(lsock,port);
      Sock::Listen(lsock);
      //初始化创建epoll模型
      epollfd = epoll_create(256);
    }

    void Addfd2Epollfd(int sock,uint32_t event)
    {
      struct epoll_event eevent;
      eevent.events = event;//设置
      if(sock == lsock)
      {
        //规定ptr为nullptr的是lsock
        eevent.data.ptr = nullptr;
      }
      else if(event & EPOLLIN) 
      {
        //其他事件获取到缓冲区和fd存放。
        eevent.data.ptr = (void*)new Bucket(sock);
      }
      //放入一个节点到红黑树
      //内部应该是拷贝该节点 --bug
      if(epoll_ctl(epollfd,EPOLL_CTL_ADD,sock,&eevent) < 0)
      {
        perror("epoll_ctl error");
      }
    }
    void DelFdFromEpoll(int fd)
    {
      epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,nullptr);
      close(fd);
    }
    void HandlerEvents(struct epoll_event eevent[],int num)
    {
      //就绪队列当中eevent已经是从0开始存放
      for(int i = 0; i < num; ++i)
      {
        uint32_t status = eevent[i].events;
        Bucket* data =(Bucket*)eevent[i].data.ptr;
        if(status & EPOLLIN)
        {
          //2种情况,监听与数据
          if(data == nullptr)
          {
            int fd = Sock::Accept(lsock);
            Addfd2Epollfd(fd,EPOLLIN);
          }
          else
          {
            //当发送塞满缓冲区向对端发送
            ssize_t s = recv(data->fd,data->buf+data->pos,sizeof(data->buf)-data->pos,0);
            if(s > 0)
            {
              //本次读取成功
              data->pos += s;
              cout << "本次读取:"<<data->buf <<endl;

              if(data->pos >= (int)sizeof(data->buf))
              {
                //协议
               //数据已经在红黑树节点,直接改变属性为发送即可
               //eevent[i].events = EPOLLOUT;
               struct epoll_event neweve;
               neweve.events = EPOLLOUT;
               neweve.data.ptr = data;
               //让发送的时候也用这个控制是否写完
               data->pos = 0;
               //Bucket* newbuk = new Bucket(data->fd);
               //neweve.data.ptr = (void*)newbuk;
               //newbuk->pos = 0;
               //strcpy(newbuk->buf,data->buf);
               //epoll_ctl(epollfd,EPOLL_CTL_MOD,data->fd,&eevent[i]);
               epoll_ctl(epollfd,EPOLL_CTL_MOD,data->fd,&neweve);
               //data->buf[0] = 0;
               //delete data;
              }
            }
            else if(s == 0)
            {
              //写端关闭或者是第三个参数为0
              //第三个参数不可能为0,只有超过低水位线才会通知
              DelFdFromEpoll(data->fd);
              delete data;
            }
            else 
            {
              //发生错误
              //TO DO
            }

          }
        }
        else if(status & EPOLLOUT)
        {
          //data是data.ptr,status是events
          //读的时候也需要考虑一次读取不上来
         //uint32_t status = eevent[i].events;
         //Bucket* data =(Bucket*)eevent[i].data.ptr;
         ssize_t s = send(data->fd,data->buf+data->pos,sizeof(data->buf)-data->pos,0);
         // ssize_t s = send(data->fd,data->buf,sizeof(data->buf),0);
          if(s > 0)
          {
            data->pos += s; 
            if(data->pos >= sizeof(data->buf))
            {
                DelFdFromEpoll(data->fd);
            }
          }
          else if(s == 0)
          {
            DelFdFromEpoll(data->fd);
            delete data;
          }
          else 
          {
            //error
            perror("recv");
            DelFdFromEpoll(data->fd);
            delete data;
          }
        }

        else 
        {
          //TO DO
        }
      }
    }
    void Start()
    {
      //将lsock放入红黑树中监听
      Addfd2Epollfd(lsock,EPOLLIN);
      //循环等待是否红黑树的节点有事件就绪
      while(1)
      {
        //OS -> usr
        struct epoll_event eevent;
        int num = 0;
        //第三个参数是我们用户层一次想处理多少个就绪请求
        switch(num = epoll_wait(epollfd,&eevent,sizeof(eevent),1000))
        {
          case -1:
            perror("epoll_wait error");
            break;
          case 0:
            printf("time out .....\n");
            break;
          default:
            HandlerEvents(&eevent,num);
            break;
        }
      }
    }
};

结果:
服务器:
【Linux】select/poll/epoll/reactor 附代码详解_第20张图片
客户端
【Linux】select/poll/epoll/reactor 附代码详解_第21张图片

epoll的工作模式

工作模式是什么:底层数据就绪之后,OS向上层进行通知的方式

水平触发:

  • epoll默认状态下就是LT工作模式. 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理;或者只处理一部分,其他等到下一次处理;

边缘触发:

  • 系统层面:通知有效次数多,并且倒逼上层将数据本次数据一次性取走;
  • 应用层面:并且可以将报文可以尽快成型,尽快被处理

【Linux】select/poll/epoll/reactor 附代码详解_第22张图片图中水平触发在水平线疯狂触发,而边缘触发只有电位变化点才会触发。

一个例子掌握水平触发和边缘触发:
若今天别人发送1KB数据, 缓冲区有1K数据;用户定义的缓冲区有0.5KB;
LT模式:读取0.5KB,或者读取部分;由于没有读取完全依旧会在下次epoll_wait当中获取就绪事件。
ET模式:一定要读取完1KB数据,当读取到0.5KB时,需要理解处理这些数据,腾出空间继续拷贝底层数据;因为本次不读取完,下次的epoll_wait会将这次事件不再提示,只有下次该文件描述符上又有新的事件才会再带过来,但是这个等待时间是未知的。

小总结:
ET:只有从无到有,从有到多才会形成一个就绪节点;并且需要将文件描述符设置为非阻塞的状态。
LT:数据可以一次性不读完;文件描述符可以设置非阻塞也可以不设置。

底层没有数据的时候就被挂起了,所以这里要用非阻塞的文件描述符。 ET模式必须这样,LT模式可以不这样,因为当读取上来刚好读完,下次要读取,会高过水位线才会允许进程去recv

编码:
struct epoll_event 的对象.events = EPOLLIN|EPOLLET;就可以显示工作在ET模式下。
recv && send 都要重新封装成,封装在bucket里面,一定要循环读取,并且fd一定要非阻塞。

【Linux】select/poll/epoll/reactor 附代码详解_第23张图片

LT模式是否需要将文件描述符设置成非阻塞:

答案:是可以设置,也可以不设置,在阻塞条件下,如果读写采用if作为条件判断,即使这次没有读取完全,由于是LT模式,底层会不停的通知有事件就绪。但是要注意,若是采用while作为条件判断,就有可能出现重复读取时,底层已经没有数据,恰好被读取完,而还在读取,此时进程就会被挂起,这个是我们不想看到的,所以我们可以权衡用哪种方式。

ET模式是否可以将文件描述符设置成阻塞:

答案:肯定不行,因为ET模式是边缘触发,即使用while读取也会碰到LT上面所说的问题,所以一定要将文件描述符设置成非阻塞。

epoll总结和零碎细节

编码注意:

  • epoll模型的就绪队列实际上也就是生产者消费者模型,用户会将就绪队列的数据往上层拿,而底层检测到事件就绪会往就绪队列当中放。
    他底层维护了epoll模型是线程安全的。
  • epoll_wait的返回值是就绪事件的个数,返回有效个数,并且依次放到我们定义的用户的缓冲区当中。也就是在这个地方,返回值是有用的。所有的遍历都是有效检测。
  • ev.data.fd要设置,因为光从events这个字段不能知道是哪个文件描述符就绪了。
  • EPOLLPRI就是如果tcp当中有个紧急指针的字段,如果报文携带了,那么epoll模型可以有这个关注这个字段。send的时候有MSG_OOB 就是带外数据,不过基本不用,因为这个维护机房主机的健康状态的时候有用。
    recv也有着个字段可以读取
  • EPOLLHUP 就是文件描述符被挂断,即对端关闭了连接。
  • 解析报文的内容也称之为反序列化。
  • 并且数据在inbuffer要不断消散,在数据放到我们用户自定义缓冲区的时候,就可以从inbuffer当中进行消散了。
  • funtional的用法就是在回调函数可以替换typedef void (*rollback_t)(Event&) 可以换成一个funtional,然后后续注册方法的时候就可以使用多种可调用对象。
  • 发送除了用 char* strart 这种方法,也可以像解析报文的时候,将发送出去的内容用erase直接删除。

问题思考:
单进程基于ET非阻塞设计的一个Reactor模式
检测事件就绪 + 对数据的读写 + 对数据的分析处理
//Linux特别常用
(检测事件就绪 + 对数据的读写) -> 对数据的分析处理(其他模块) 半同步半异步的Reactor 反应堆模式
// 下面的方式,Linux特别不常用
(检测事件就绪,分发事件) + 对数据的读写 + 对数据的分析处理, proactor前摄式;
// 谁规定,我们的服务器只能有一个Reactor??可以存在多个吗??
// 可以创建多个线程或者进程,每一个进程或者线程都new 一个reactor可以吗??
// 只有listen_sock是被所有的进程和线程共享的其他的可以私有给每一个线程或者进程吗?

Reactor

本质:就是就是事件就绪后调用提前注册的方法,这个方法和具体的业务有关。

解决上述以往的问题:

  • 通过每一个套接字配一个string outbuffer,inbuffer来实现有输入输出缓冲区,并且通过定制协议来保证不会发生粘包。
  • 每一个文件描述符都设置成非阻塞,读写的时候即使没有读完,也会对返回值做判断进行重复读。
  • 并且采用了Reactor的方式对Epoll模式进行了封装,变得更加高效。

编码注意:

  • 由于ET模式,在读取内核数据的时候,有可能会碰到信号,此时需要继续读取,不然若没有下一次读取,本次数据就丢失了。不仅是数据,在底层连接获取的时候,也需要重复accept,不然就会可能连接长时间得不到获取。
  • 而listen_sock需要将底层连接获取上来,需要循环,而事件sock也需要设置为ET模式,然后套接字也需要设置成非阻塞读取。
  • 这里用户自定义协议,用X代表两个算式的分隔,目前只实现加法。

epoll测试2

用ET模式实现一个网络计算器

Epoll.hpp

#pragma once

#include
#include 
#include
#include
#include
namespace ns_Epoll{
    class Epoll;
    class Event;
using std::cout;
using std::endl;
    typedef void(*rollback_t)(Event&);

    class Event{
        public:
            int _fd;
            Epoll* _r;

            std::string _inbuffer;
            std::string _outbuffer;

            rollback_t _readRollback;
            rollback_t _writeRollback;
            rollback_t _errorRollback;
        Event():_fd(-1),_r(nullptr),_readRollback(nullptr),_writeRollback(nullptr),_errorRollback(nullptr)
        {}
    };
        
    class Epoll{
        private:
            int _epoll_fd;
            std::unordered_map<int,Event> _fd_to_event;

        public:
        //删除事件,在对端关闭连接的时候才会调用 如Read事件当中Read到0,以及发送报文的时候发送失败。
        void DelEvent(int fd)
        {
          std::cout << fd << " is del event" << std::endl; 
          close(fd);
          epoll_ctl(_epoll_fd,EPOLL_CTL_DEL,fd,nullptr);
          _fd_to_event.erase(fd);
        }

        void ChangeEventMode(int fd,uint32_t events)
        {
          cout << "fd :" <<fd << "change mode" << std::endl;
          struct epoll_event myevent;
          myevent.data.fd = fd;
          myevent.events = events;
          epoll_ctl(_epoll_fd,EPOLL_CTL_MOD,fd,&myevent);
        }
        void InitEpoll()
        {  
            //创建Epoll模型
            _epoll_fd = epoll_create(128);
            //std::cout << " epoll fd is " << _epoll_fd << std::endl;
            if(_epoll_fd < 0)
            {
              std::cerr << "_epoll_fd" ;
              exit(2);
            }
        }

        //AddEvent提供添加进Epoll模型的事件
        void AddEvent(const Event& event,uint32_t events)
        {
          
         
          int fd = event._fd;
          std::cout << fd << "is Add Event" << std::endl;
          struct epoll_event eve;
          //std::cout << fd << " is fd" << std::endl;
          //每次需要将对应的套接字与事件进行关联
          _fd_to_event[fd] = event;
          eve.data.fd = fd;
          //默认关心读事件,写事件选择关心
          eve.events = events | EPOLLET;
          epoll_ctl(_epoll_fd,EPOLL_CTL_ADD,fd,&eve);
        }
        bool IsExist(int sock)
        {
          if(_fd_to_event.find(sock) != _fd_to_event.end())
          return true;

          return false;
        }
        
        //说到底就是将Epoll模型当中的就绪事件做处理
        void Dispatch(int timeout)
        {
#define NUM 10
          struct epoll_event events[NUM];
          int num = epoll_wait(_epoll_fd,events,NUM,timeout);
          //std::cout << num <
          // sleep(1);
          for(int i = 0;i < num;++i)
          {
           //由于EPoll模型中都是从AddEvent来的,所以里面的方法都是注册好的
            int fd = events[i].data.fd;
            //std::cout << "fd" << " :" << fd << std::endl;
            Event& ev = _fd_to_event[fd];
            
            //错误都由读写事件来检测
            if(events[i].events & EPOLLERR) events[i].events |= (EPOLLIN|EPOLLOUT);
            if(events[i].events & EPOLLHUP /*对端关闭链接*/)  events[i].events |= (EPOLLIN|EPOLLOUT);

            //判断为什么事件就绪
            if(IsExist(fd) && EPOLLIN & events[i].events)
            {
              if(ev._readRollback)
              ev._readRollback(ev);
            }

            if(IsExist(fd) && EPOLLOUT & events[i].events)
            {
              if(ev._writeRollback)
              ev._writeRollback(ev);
            }
            
            if(IsExist(fd) && EPOLLERR & events[i].events)
            {
              if(ev._errorRollback)
              ev._errorRollback(ev);
            }
          }
        }

    };
}

Socket.hpp

#include 
#include 
#include 
#include 
#include
#include 
#include 
#include 
#include 

namespace ns_socket
{
    class Socketer{
        public:
        static int Socket()
        {
            int sock = socket(AF_INET,SOCK_STREAM,0);
            if(sock < 0)
            {
              //std::cerr << "socket";
              exit(2);
            }
            int opt = 1;
            setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
            return sock;
        }
        static void Bind(int sockfd,int port)
        {
            struct sockaddr_in local;
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = INADDR_ANY;
            local.sin_port = htons(port);
            
            if(bind(sockfd,(struct sockaddr*)&local,sizeof(local)) < 0)
            {
              std::cerr << "bind";
              exit(2);
            }
        }
        static void Listen(int sockfd)
        {
            #define BACKLOG 5
            listen(sockfd,BACKLOG);
        }
        static bool  SetNonBlock(int fd)
        {
            int fl = fcntl(fd,F_GETFL);
            if(fcntl(fd,F_SETFL,fl|O_NONBLOCK) == -1)
            return false;

            return true;
        }
    };
}

RollBack.hpp

 #pragma once
#include
#include"Epoll.hpp"
#include"Util.hpp"
#include"Socket.hpp"
#include
#include
#include
using std::vector;
using std::string;
using std::cout;
using std::endl;
using namespace ns_Epoll;
using namespace ns_socket;
//普通套接字的回调函数

//网络版的一个计数器
void Reader(Event& eve)
{
  //不读取上来会一直报这个错误
  std::string& inbuffer = eve._inbuffer;
  int fd = eve._fd;
  //由于保证读取,所以这里需要设置为非阻塞
  Socketer::SetNonBlock(fd);
  std:: cout << fd << std::endl;
  char ch;
  ssize_t s = 0;
  while((s = read(fd,&ch,1)) >= 0)
  {
    //读取的时候需要判断条件
    if(errno == EINTR)
    {
      continue;
    }
    else if(errno == EAGAIN || errno  == EWOULDBLOCK)
    {
      //当设置EAGAIN时,发送缓冲区满了就会返回错误码
      eve._r->ChangeEventMode(fd,EPOLLIN| EPOLLOUT);
      return ;//此时不往下继续读了
    }
    else if(s == 0)
    {
      //客户端关闭连接
      std::cout << "fd:" << fd <<"is quit..."<< std::endl;
      eve._r->DelEvent(fd);
      return ;
    }
    inbuffer += ch;
  }
  //这之前都是存放在Event当中的缓冲区的。
 
  std::cout << "client send mess: " << inbuffer << std::endl;
  //这里就是对inbuffer当中的内容进行处理的
  //自定义协议 1+1X2+2X
  vector<string> partvr;
  //将inbuffer数据切割成单个1+1这种
  Util::CutString(inbuffer,partvr,"X");
  //CutString若后面出现1+就没有的情况,是不会放入partvr,此时数据依旧在inbuffer当中 
  string& outbuffer = eve._outbuffer;
  //debug
  // cout << inbuffer << endl; 


  //走到这里,没发完的报文都在inbuffer,发完的在这里构建回应
  for(auto& str:partvr)
  {
    //debug
    // std::cout << str << " ";
    // fflush(stdout);

    //存储最终的答案,临时存储两个字符串 “1” + “1” 类似这种,最终将结果放到outbuffer当中
    vector<string> tmpvr;
    string tmp = str;
    Util::CutStringForOp(str,tmpvr,"+");//这里我只处理这种情况,后续增加 TODO
    cout << tmpvr.size() << endl;
    if(tmpvr.size() != 2)
    {
      //当输入的串不合法就会走到这里
      std::cout << "debug" << " tmpver.size() != 2,str = " << str << std::endl; 
      continue;//这种情况不满足协议,丢弃
    }
    int res = stoi(tmpvr[0]) + stoi(tmpvr[1]);
    cout << res << endl;
    //构建回应
    outbuffer += tmp;
    outbuffer += "=";
    std::stringstream ss;
    ss << res;
    string a ;
    ss >> a;
    outbuffer += a;
    outbuffer += "X";
    outbuffer += "\n";
  }
  eve._r->ChangeEventMode(fd,EPOLLIN|EPOLLOUT
 | EPOLLET);
  //什么时候构建回应,什么时候添加回应的字段回去

}
void Writer(Event& eve)
{
  //不读取上来会一直报这个错误
  int fd = eve._fd;
  //由于保证读取,所以这里需要设置为非阻塞
  Socketer::SetNonBlock(fd);
  std::string outbuffer = "Server echo#";
  outbuffer += eve._outbuffer;
  eve._outbuffer = outbuffer;

  //回应的格式
  int size = outbuffer.size();
  int left = size;
  int onewrite = 0;
  char* start = (char*)outbuffer.c_str();
  std::cout << outbuffer << std::endl;
  while((onewrite = write(fd,start,left)) > 0){
    // sleep(2);
    //std::cout << left << " "  << onewrite << " " <
    //由于ET模式,写的时候可能是
    if(errno == EINTR)
    {
      //被信号杀了,下次还要读,所以不需要更改事件的状态
      return;
    }
    else if(errno == EAGAIN || errno == EWOULDBLOCK)
    {
      //表示这次读取
      //表示需要继续读取东西才能继续写
      
      eve._r->ChangeEventMode(fd,EPOLLIN);
      return ;
    }
    // else if(onewrite == 0)
    // {
    //   //对端关闭连接,当前连接可以被关闭,并且从epoll模型拿下来,从哈希表当中拿下来
    //   //为了保证Epoll本身封装性,所以添加一个函数
    //   eve._r->DelEvent(fd);
    // }
    left -= onewrite;
    start += onewrite;
    eve._outbuffer.erase(0,onewrite);
  }
  write(fd,"\n",1);
  eve._r->ChangeEventMode(fd,EPOLLIN|EPOLLET);
  

}
void Errorer(Event& eve)
{
  std::cout << "出现了错误,调用了错误处理函数" << std::endl;
  eve._r->DelEvent(eve._fd);
}

//listen套接字的专属回调函数
void ListenRollBack(Event& eve)
{
  //ET模式下需要不断读取不然会有可能连接长期获取不上来
  while(true)
  {
    std::cout << " listenRollBack start " << std::endl;
    //当Epoll当中检测监听套接字有数据
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int sock = eve._fd;
    int fd = accept(sock,(struct sockaddr*)&peer,&len);
    if(fd < 0)
    {
      break;
    }
    Epoll* r = eve._r;
    //每次监听套接字有接受数据也要将其他套接字的方法设置了
    Event event;
    event._fd = fd;
    event._r = r;
    event._readRollback = Reader;
    event._writeRollback = Writer;
    event._errorRollback = Errorer;
    r->AddEvent(event,EPOLLIN | EPOLLET)
;
  }

}


Util.hpp

#pragma once

#include
#include
#include
using std::vector;
using std::string;

namespace Util{
    void CutString(string& instr,vector<string>& resultvr,string cnt)
    {
        //根据 cnt进行将instr进行拆分
        //这里instr是Event当中的inbuffer字符串,这个字符串由于是ET模式,所以切割完后可能有部分没有切割成功
        //所以我们将切割成功的用resultvr进行返回,将instr切割成功部分进行删除
        while(true)
        {
            size_t index = instr.find(cnt);
            if(index == instr.npos)
            {
                //就说明这次切割失败,下次再切分
               return ; 
            }
            else 
            {
                // 1+1X2+2X3+
                resultvr.push_back(instr.substr(0,index));//这里插入1+1到CutString当中
                instr.erase(0,index+1);//表示删除的时候将协议的X也删除
            }
        }
        
    }
    //需要判断每个子串是否合法
    void CutStringForOp(string& instr,vector<string>& resultvr,string cnt)
    {
        //根据 cnt进行将instr进行拆分
        //这里instr是Event当中的inbuffer字符串,这个字符串由于是ET模式,所以切割完后可能有部分没有切割成功
        //所以我们将切割成功的用resultvr进行返回,将instr切割成功部分进行删除
        while(true)
        {
            size_t index = instr.find(cnt);
            if(index == instr.npos)
            {
                //就说明这次切割失败,下次再切分
               return ; 
            }
            else 
            {
                // 1+1X2+2X3+
                string str1 =instr.substr(0,index);
                string str2 =instr.substr(index+1);
                if(!str1.empty())
                resultvr.push_back(str1);//这里插入1+1到CutString当中
                if(!str2.empty())
                resultvr.push_back(str2);//这里插入1+1到CutString当中
                //合法和不合法都直接清理掉
                instr = "";
            }
        }
        
    }
}

Server.cc

#include"Epoll.hpp"
#include"Socket.hpp"
#include
#include
#include
#include"RollBack.hpp"
using namespace ns_socket;
using namespace ns_Epoll;



void Usage(char* process){
  std::cout <<  process << " :port" << std::endl;
}
int main(int argc,char* argv[])
{
  if(argc != 2){
    Usage(argv[0]);
    exit(1);
  }
  Epoll* p = new Epoll;
  p->InitEpoll();

  //启动服务
  int listen_sock = Socketer::Socket();
  Socketer::Bind(listen_sock,atoi(argv[1]));
  Socketer::Listen(listen_sock);
  Socketer::SetNonBlock(listen_sock);

  //设置监听套接字进EPoll模型
  Event eve;
  eve._fd = listen_sock;
  eve._r = p;
  eve._writeRollback = nullptr;
  eve._errorRollback = nullptr;
  eve._readRollback = ListenRollBack;
  p->AddEvent(eve,EPOLLIN|EPOLLET);
  int timeout = -1;
  while(true)
  {
    //std::cout << "Dispatch" << std::endl;
    p->Dispatch(timeout);
  }
}

Makefile

server:server.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f server 

结果:
【Linux】select/poll/epoll/reactor 附代码详解_第24张图片

注意:

  • 若X作为协议不合适,可以用ascii当中不可显的字符作为协议,如一些报文当中不可显的如\+数字来替换。

总结:

  • 真正的业务处理只有RollBack.hpp当中Reader方法的将数据放到inbuffer后面的一部分。当需要更改为其他业务,只需要更改这一段即可。
  • 可靠性:水平相对可靠,不会丢失数据;而ET模式下这套写法可靠性也能得到保证。

插播小问题:为何之前事件就绪不读取,会疯狂提醒?
poll/select/epoll事件发生的时候,若没有调用read,就会一直认为事件发生,因为他们默认都是LT模式



看到这里,实际上已经讲完了,剩下的只有总结!!!

理解与总结


前提知识:

  • 网卡数据在到达底层,会发生中断拷贝到我们的内核缓冲区,在到达了一个数据的低水位线,操作系统才会通知上层让进程把数据拷贝走;这样做是为了减少IO次数,让单次能够拷贝的数据更多。

  • 其中mmap就是建立一个文件能够直接映射内核缓冲区的一部分,当往内核缓冲区写,文件就能够直接看见,这样子数据就不会冗余,并且不需要从内核拷贝到用户。

  • 在互斥量,条件变量,当锁资源没有就绪,我们经常说进程或线程会被挂起,此时他们就会挂起到锁的一个等待队列。那么文件读取的时候有没有可能读阻塞或者写阻塞呢?有可能,那么每个文件都会维护一个等待队列!

  • Linux下一切皆文件,struct file当中有一个函数指针数组file_operations,文件的函数指针当中有poll方法。

  • 上层调用select或poll,实际上是调用底层的poll或select(驱动)方法。

【Linux】select/poll/epoll/reactor 附代码详解_第25张图片

中断的理解:

  • 键盘没按的时候,按的时候操作系统能够立马识别,这种硬件技术叫做中断,cpu是有针脚,外设虽然不能直接与cpu进行数据交互,但是可以进行事件交互,当有事件来时,可以向cpu发起某些中断。类似硬件级别的信号,就是一次电脉冲。cpu识别针脚的编号就可以反应是哪一个编号需要执行,也就有中断号,中断向量表,中断上下文的概念。
  • 网卡也如此,网卡有数据的时候向cpu发送信号(不是前面的那种信号),操作系统可以识别cpu上的情况,就可以知道有数据来了,把数据从外设搬运到内存当中。

操作系统怎么知道哪些数据可以交付给上层,即事件就绪通知:

  • 操作系统维护高低水位线概念,网卡当有数据时,通过中断放入内核缓冲区,当数据越来越多直到低水位线,OS才会通知上层数据已经就绪了。

操作系统的读写就绪概念:

  • 读事件:socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记 SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
    socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
    监听的socket 上有新的连接请求;
    socket上有未处理的错误;
  • 写事件:socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
    socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
    socket使用非阻塞connect连接成功或失败之后;
    socket上有未读取的错误;

非阻塞轮询的本质:

  • 非阻塞轮询实际上就是对底层数据是否达到低水位线。就会将进程唤醒,再告知进程将数据从内核缓冲区拷贝到用户缓冲区。这个水位线才是真正被轮询检测。

阻塞检测的本质:

  • 将进程设置进文件的等待队列当中,事件就绪后进程调用底层的poll方法,这也是回调方法,但是做的事件比epoll少。

阻塞等待 vs 非阻塞

  • select/poll阻塞,非阻塞概念:进程pcb放到要每一个等待文件的等待队列,一有网卡数据到达低水位线就绪就唤醒进程;而非阻塞是自己下去看,不把进程放到等待队列,同样也是检测数据是否到达低水位线。 二者检测底层数据就绪都是调用文件的poll方法,把revents设置或者是fd_set设置,然后返回值加加。
  • epoll阻塞,非阻塞概念:阻塞时,epoll会在就绪队列中等待,等就绪队列当中有事件OS唤醒进程,进程调用对应方法,拷贝数据到用户。非阻塞即轮询检测就绪队列是否有数据。

epoll高效的本质:

  • select,poll的就绪事件检测时,哪些事件就绪是需要操作系统轮询检测的。这个过程要检测到没有就绪的文件描述符,所以效率不高,因为select,poll都需要遍历设置的所有文件描述符才能知道哪些事件已经就绪。;而epoll由于注册了网卡驱动方法,所以就绪的事件会到就绪队列,OS轮询检测的时候看就绪队列是否有节点即可。
  • select,poll每次循环调用都要重新将要关心的事件设置进内核,即阻塞的话要到每个文件描述符下的等待队列等待,非阻塞轮询需要检测所有要关心的文件描述符。
  • epoll的对于文件描述符的事件关心只需要在第一次设置(epoll只需要在第一次的时候进行一个驱动级回调函数的设置),对红黑树进行增加节点,后期只需要看就绪队列即可,也不需要重新设置就绪事件,因为这个工作内核帮用户做了。
  • epoll的注册方法比起select,poll,就是在文件数据就绪,就把数据放到就绪队列,然后拷贝到用户定义的缓冲区当中。比起其他两种都多做了将数据从内核拷贝到用户的环节。epoll对比起其他两种就是通过新增数据结构的方式,减少了遍历检测的成本。

多路转接这么厉害,什么时候都能用吗?

  • 使用场景:长链接
  • 原因:短连接的情况频繁对红黑树进行操作,因为短链接在连接建立的比较频繁的。

注意点:

  • 说使用mmap减少内核就绪队列拷贝到用户都是错误的,底层没有使用内存映射机制。并且epoll_event这个数组是在用户定义的。
  • 如果底层数据没有到达低水位线,不管是阻塞还是轮询,上层都是没有反应的。

你可能感兴趣的:(Linux,linux,运维,服务器)