目录

IO的定义

IO效率是什么

谈个题外话——运行效率

IO模型的介绍

阻塞式IO模型

概念

实现

 题外话——设置非阻塞

非阻塞式IO模型

概念

实现

信号驱动IO模型

概念

多路转接(复用)模型

概念

异步IO模型

概念

题外话——同步和异步

多路转接之select

select的接口

select的原理

select的使用

使用select编写简单服务器

select的缺点

多路转接之poll

poll的接口

poll的原理

poll的使用

使用poll编写简单的服务器

poll的特点

多路转接之epoll

epoll的接口

epoll的原理

epoll的使用

epoll的工作模式

LT(Level-triggered)

ET(Edge-triggered)

ET模式比LT模式更加高效

EPOLL的特点

使用poll编写简单的服务器

后记


IO的定义

提到IO,大家应该耳熟能详:站在整个计算机的角度,I就是从网卡读数据,O就是向网卡写数据。但我们这里关注点并不在计算机,而是在于提供服务的程序,或者直白来说,进程的IO效率如何,这是评价客户端/服务器架构的程序好坏的标准之一。

站在进程的角度来说:I就是从文件缓冲区里读数据(本质就是拷贝数据),O就是向文件缓冲区里写数据(本质也是拷贝数据)

但是进程并不是一直在读/写数据的,甚至于说,进程大部分时间都在“等”,等待数据包从网络上传输过来,等待发送缓冲区从满变为不满等等。举个简单的例子:当通信双方建立一个TCP长连接之后,假设一方一直不输入,那另一方就一直处于等待状态。

因此我们把IO定义的更准确一些——IO = “等”+拷贝,请一定要记住这一点

注:我们把IO时引起等待状态与拷贝状态的切换的事件(比如缓冲区不满了)叫做一次IO事件


IO效率是什么

一个高效率的IO是怎样的,要分析这个问题,我们得回归IO的定义上:IO = “等”+拷贝。

是将等待的时间缩短,让进程几乎一直处在拷贝状态以达到单位时间内发送更多数据的目的吗?

  • 是的,但是不可能。因为“等”的时间几乎不取决于自己,而取决于对方。对方不发消息的时候难道还可以把他的手机抢过来帮他发吗?缩短“等”的时间不现实。

是将拷贝的速率提高,让进程单位时间内可以拷贝更多数据吗?

  • 是的,但是方向错了。首先,在IO过程中,“等”的时间是大概率占绝大部分的,提升拷贝速率有些鸡肋。其次拷贝速率的提高与硬件有关,而硬件是有上限的,且还得考虑成本。

IO效率就是通信系统有效IO时间(拷贝时间)和总数据传输时间比值。在多线程模型其实我思考过,由于cpu不会做无意义的等待,等待读的线程会被挂起,那么cpu在整个数据传输过程里,cpu是一直在进行有效拷贝,和多路转接差不太多。但是第一空间消耗太大。第二调度开销太多。所以还是多路转接模型更好。虽然说多路转接模型单线程可能会导致对某些用户反应不及时,但是可以创建多个使用多路转接模型的线程进行负载均衡

具体来说:

同样的一万个IO需求,如果我们把他们交给一万个线程分别处理,那可以想象,线程爆炸了,且不说频繁的线程切换以及每个线程等待每个IO就绪带来的时间消耗,光是这一万个线程所需要的内存空间也不是小数目。

同样的问题,如果我们使用一个线程来处理这一万个IO需求,我们从整体角度来分析一下,整个系统“等”的时间依旧是这一万个IO中最长的等待时间,拷贝的速率也没变,但是由于在一些IO的等待过程中,这个线程可以处理其他IO的拷贝,因此无意义的等待时间被大大减少了。并且我们只用了一个线程。这种方式大大降低了时间和空间成本


谈个题外话——运行效率

我们经常会认为,非阻塞式的IO比阻塞式的IO效率高,但其实这是一种错误的说法,阻塞与非阻塞只是等待的方式不同,既没有缩短等待时间或者提高拷贝速率,也没有提高单个线程所能处理的需求数

只是由于非阻塞方式可以让程序在等待过程中做一些其他的事情而不是干等着,使得整个程序的运行效率变高了(能处理更多的事情),这和IO效率可没什么关系

总之,非阻塞式IO与阻塞式IO的IO效率相同。


IO模型的介绍

阻塞式IO模型

概念

在没有数据的时候进程阻塞等待,有了数据后进程被唤醒并进行拷贝。这是最常用的模型,虽然IO效率和运行效率都不高(因为足够简单)。

实现

#include<iostream>
#include<unistd.h>

using namespace std;

int main()
{
    while(1)
    {
        char buffer[100] = {0};
        read(0,buffer,sizeof(buffer));//读取键盘文件的数据
        cout<<buffer<<endl;
    }
    return 0;
}

 题外话——设置非阻塞

只要把一个文件的fd设置成非阻塞模式,那么当我们读取这个文件的时候就是非阻塞,具体如下:

#include <unistd.h>
#include <fcntl.h>
void SetNoBlock(int fd) {
    int fl = fcntl(fd, F_GETFL);//得到fd指向的文件的旧的标志位(一个位图)
    if (fl < 0) {//说明获取标志位失败了
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);//设置本文件的标志位(把非阻塞添加到标志位中)
}

非阻塞式IO模型

概念

如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回 EWOULDBLOCK 错误码,一般要轮询检查直到数据准备好。

实现

#include<iostream>
#include<unistd.h>

using namespace std;

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
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 read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size > 0)
        {
           cout<<"收到数据:"<<buf<<endl;
        }
        else if(read_size == 0)
        {
           cout<<"读到了文件结尾"<<endl;
           break;
        }
        //被信号中断,read出错,数据没准备好,这些情况都是返回-1,为了区分就是用errno错误码
        else if(read_size < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
        {
           cout<<"底层数据没有准备好,继续等待"<<endl;
           //可以做其他事
        }
        else if(read_size < 0 && errno == EINTR)
        {
           cout<<"读取被信号中断,重新尝试"<<endl;
        }
        else
        {
           cout<<"read出错"<<endl;
        }
        sleep(1);
    }
    return 0;
}

信号驱动IO模型

概念

关于这个模型,不做实现,但它实际上就是让用户手动为SIGIO信号设置回调处理函数(数据报准备好后OS会给对应进程发送信号SIGIO,但默认该信号没有绑定回调处理函数),进程接收到信号后就才拷贝数据。


多路转接(复用)模型

概念

多路转接是指一个线程同时对多个文件描述符进行等待,哪些文件就绪了就对它们进行读写等操作,按照我们上面对IO效率的定义,多路转接实际上就提高了IO效率。关于它的实现方式我们下面会详细讲解

多路转接提高了IO效率,也是我们下面主要讲述的模型


异步IO模型

概念

这个模型我们也去不实现。异步是指整个IO过程都不是由调用者自己来做的而是交给操作系统来做。调用者唯一要做的就是触发IO并且使用数据,等待和拷贝都是由OS做。


题外话——同步和异步

在IO中:

  • 异步是指整个IO过程都不是由调用者自己来做的,调用者唯一要做的就是触发IO并且使用数据,等待和拷贝都是由别人去做。
  • 同步是指IO过程至少有一步是由调用者自己来做,比如在信号驱动IO中,等待的过程不是自己做的,但是拷贝是自己来拷的,这也叫同步。

在多线程中:

  • 同步指的是多个线程的执行具有先后关系。
  • 异步指的是多个线程的执行顺序无法保证。

因此,在IO这里所说的同步和异步与线程中的同步和异步没有任何关系


多路转接之select

select是OS提供给我们的实现多路转接的接口,他本质上是一种就绪通知机制当有文件就绪的时候,他会通知上层,但并不会帮用户进行数据拷贝

select的接口

select的原理

select的原理很简单,它会对传入的所有fd进行线性遍历,看看哪些fd在用户关心的事件上就绪了(比如查看某fd的缓冲区是否有数据,如果有并且用户关心读事件,select就通知调用者该fd读就绪了),遍历完成把所有就绪的fd通知给调用者。

select的使用

  • 由于readfds等参数是输入输出型参数,一个数组充作两用,既要传递参数又要保存结果,因此每次调用select时都要重新设置你要关心哪些fd的哪些事件。为了记录关心某事件的fd有哪些,用户需要自己维护三个fd数组(对应三个不同的事件)
  • 相同的fd可以同时出现在readfds,writefds,execptfds中,他们是各自管不同的领域的(读就绪,写就绪,异常)。
  • 由于fd_set本质是一个固定的数组位图,所以select能关注的fd数量是有限的
  • 系统提供了对fd_set类型变量进行操作的宏函数:

使用select编写简单服务器

我们这里只是介绍一下select的使用,所以服务器使用面向过程的方式编写,并且我们也只使用了readfds,后面会编写一个完整的服务器。

#include<iostream>
#include<unistd.h>
#include <sys/types.h>      
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include<string>
#include <strings.h>
using namespace std;

#define NUM (sizeof(fd_set)*8)
int fdarr[NUM];//这个数组是用来记录select要关注哪些fd,因为select的readfds是输入输出型参数,会被修改,所以我们必须把需要关注的fd记录下来
int fdmax = -1;//记录在所有要关心的fd中,值最大的fd

//用于获取链接
void Accept(int fd)
{
   struct sockaddr_in addr;
   socklen_t len= sizeof(addr);
   bzero(&addr,len);
   //获取新连接
   int newfd = accept(fd,(struct sockaddr*)&addr,&len);
   //将新的文件描述符的fd也存入fdarr让select关注它
   int pos = -1;
   //找一个空位置
   for(int i = 0;i<NUM;i++)
   {
      if(fdarr[i] == -1)
      {
         pos = i;
         break;
      }
   }
   if(pos == -1)
   {
      cout<<"服务器满了(select容量耗尽)"<<endl;
      return;
   }

   cout<<"获取了新连接:"<<newfd<<endl;
   if(newfd>fdmax)fdmax = newfd;
   fdarr[pos] = newfd;
}

//用于读取数据
void Recv(int who)
{
   char buffer[1024] = {0};
   int n = recv(fdarr[who],buffer,sizeof(buffer),0);
   if(n<0)
   {
      cout<<"读取出错.........."<<endl;
   }
   else if(n == 0)
   {
      cout<<"有一个用户退出........";
      close(fdarr[who]);
      fdarr[who] = -1;
   }
   else
   {
      buffer[n] = 0;
      cout<<"收到一条信息:"<<buffer<<endl;
      string message = "echo#";
      message+=buffer;
      send(fdarr[who],message.c_str(),message.size(),0);
   }
}

void Dispatcher(fd_set* result,int listenfd)
{
   //遍历fdarr,看看哪些fd就绪了
   for(int i = 0;i<NUM;i++)
   {
      //这个位置没有存放fd
      if(fdarr[i] == -1)continue;
      
      //这个位置有fd并且fd就绪了
      if(FD_ISSET(fdarr[i],result))
      {
         //这个位置存放的是监听套接字的fd
         if(fdarr[i] == listenfd)
         {
            Accept(fdarr[i]);
         }
         //这个位置存放的是普通套接字的fd
         else
         {
            Recv(i);
         }
      }
    
   }
}


int main(int argc,char * argv[])
{
   if(argc<2)
   {
      cout<<"请给出端口号"<<endl;
      return 0;
   }
   //创建用于监听的套接字
   int listen_fd = socket(AF_INET,SOCK_STREAM,0);

   //创建套接字的标识信息结构体
   struct sockaddr_in addr;
   bzero(&addr,sizeof(addr));
   addr.sin_family = AF_INET;
   addr.sin_port = htons(stoi(argv[1]));
   addr.sin_addr.s_addr = INADDR_ANY;

   //设置地址重用
   int reuse = 1;
   setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

   //将标识信息结构体绑定到套接字上
   bind(listen_fd,(struct sockaddr*)&addr,sizeof(addr));

   //开启监听
   listen(listen_fd,5);

   //初始化fdaddr
   for(int i = 0;i<NUM;i++)
   {
      fdarr[i] = -1;
   }
   
   //监听套接字获取链接也是一种IO,因此把监听套接字也用select管理起来
   fdarr[0] = listen_fd;
   fdmax = listen_fd;

   while(1)
   {
      fd_set readfds;
      FD_ZERO(&readfds);
      //每次都重新设置要关心的fd
      for(int i = 0;i<NUM;i++)
      {
         if(fdarr[i] != -1){
         FD_SET(fdarr[i],&readfds);}
      }

      //调用select等待有fd就绪
      struct timeval wtime = {2,0};
      int ret = select(fdmax+1,&readfds,nullptr,nullptr,&wtime);
      if(ret>0)
      {
         //提取就绪的事件
         Dispatcher(&readfds,listen_fd);
      }
      else if(ret == 0)
      {
         cout<<"没有事件就绪"<<endl;
      }
      else
      {
         cout<<"select error"<<endl;
      }
   }

}

我们要明白,select就是就绪通知机制,他做的只是以阻塞/非阻塞方式检查一堆fd中哪些fd就绪并通知服务器。

select的缺点


多路转接之poll

poll的接口

poll的原理

poll的原理和select是一样的,主要是接口不同。

poll的使用

  • poll依旧需要用户维护一个辅助数组来记录要关心哪些fd的哪些事件,但这个辅助数组的大小不是固定的,可以动态扩展。
  • 相对于select来说,poll是单独给每个fd设置关心哪些事件,而不是在某事件下添加fd。
  • 对于不合法的fd,poll会忽略。

使用poll编写简单的服务器

#include<iostream>
#include<unistd.h>
#include <sys/types.h>      
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <poll.h>
#include<string>
#include<string.h>
#include <strings.h>
using namespace std;

#define NUM 1

struct pollfd *fdarr;//这个数组是用来记录select要关注哪些fd,因为select的readfds是输入输出型参数,会被修改,所以我们必须把需要关注的fd记录下来
int nums = 0;//记录fdarr的元素数量

void Accept(int fd)
{
   struct sockaddr_in addr;
   socklen_t len= sizeof(addr);
   bzero(&addr,len);
   //获取新连接
   int newfd = accept(fd,(struct sockaddr*)&addr,&len);
   //将新的文件描述符的fd也存入fdarr让select关注它
   int pos = -1;
   //找一个空位置
   for(int i = 0;i<nums;i++)
   {
      if(fdarr[i].fd == -1)
      {
         pos = i;
         break;
      }
   }
   if(pos == -1)
   {
      //服务器满了,扩容!
      struct pollfd *temp = new pollfd[2*nums];

      for(int i = 0;i<2*nums;i++)
      {
         temp[i].fd = -1;
         temp[i].events = 0;
         temp[i].revents = 0;
      }
      memcpy(temp,fdarr,sizeof(struct pollfd)*nums);
      delete[] fdarr;

      fdarr = temp;
      nums*=2;
      return;
   }

   cout<<"获取了新连接:"<<newfd<<endl;
   fdarr[pos].fd = newfd;
   fdarr[pos].events |= POLLIN;
   fdarr[pos].revents = 0;
}

void Recv(int who)
{
   char buffer[1024] = {0};
   int n = recv(fdarr[who].fd,buffer,sizeof(buffer),0);
   if(n<0)
   {
      cout<<"读取出错.........."<<endl;
   }
   else if(n == 0)
   {
      cout<<"有一个用户退出........"<<endl;
      close(fdarr[who].fd);
      fdarr[who].fd = -1;
   }
   else
   {
      buffer[n] = 0;
      cout<<"收到一条信息:"<<buffer<<endl;
      string message = "echo#";
      message+=buffer;
      send(fdarr[who].fd,message.c_str(),message.size(),0);
   }
}

void Dispatcher(int listenfd)
{
   //遍历fdarr,看看哪些fd就绪了
   for(int i = 0;i<nums;i++)
   {
      //这个位置没有存放fd
      if(fdarr[i].fd == -1)continue;
      
      //这个位置有fd并且fd读就绪了
      if((fdarr[i].revents & POLLIN) > 0)
      {
         //这个位置存放的是监听套接字的fd
         if(fdarr[i].fd == listenfd)
         {
            Accept(fdarr[i].fd);
           
         }
         //这个位置存放的是普通套接字的fd
         else
         {
            Recv(i);
         }
      }
      //这个位置有fd并且fd写就绪了
      else
      {
         //......
      }
    
   }
}


int main(int argc,char * argv[])
{
   if(argc<2)
   {
      cout<<"请给出端口号"<<endl;
      return 0;
   }
   //创建用于监听的套接字
   int listen_fd = socket(AF_INET,SOCK_STREAM,0);

   //创建套接字的标识信息结构体
   struct sockaddr_in addr;
   bzero(&addr,sizeof(addr));
   addr.sin_family = AF_INET;
   addr.sin_port = htons(stoi(argv[1]));
   addr.sin_addr.s_addr = INADDR_ANY;

   //设置地址重用
   int reuse = 1;
   setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

   //将标识信息结构体绑定到套接字上
   bind(listen_fd,(struct sockaddr*)&addr,sizeof(addr));

   //开启监听
   listen(listen_fd,5);

   //初始化fdaddr
   fdarr = new pollfd[NUM];
   nums = NUM;
   for(int i = 0;i<nums;i++)
   {
      fdarr[i].fd = -1;
      fdarr[i].events = 0;
      fdarr[i].revents = 0;
   }
   
   //监听套接字获取链接也是一种IO,因此把监听套接字也用select管理起来
   fdarr[0].fd = listen_fd;
   fdarr[0].events |= POLLIN;

   while(1)
   {
      int ret = poll(fdarr,nums,-1);
      if(ret>0)
      {
         //提取就绪的事件
         Dispatcher(listen_fd);
      }
      else if(ret == 0)
      {
         cout<<"没有事件就绪"<<endl;
      }
      else
      {
         cout<<"poll error"<<endl;
      }
   }

}

poll的特点

poll解决了select的一些问题:

  • fds由用户定义,可以任意大小,因此支持的关心的fd数量理论上是没有限制的。
  • select将一个位图充作两用,所以每次输入时都要重新设置关心位图,而poll每个文件描述符对应两个位图,一个用作输入,一个用作输出,互不干扰,所以不用每次都设置,接口使用更方便

poll缺点:

  • 依旧需要在内核层线性遍历fd来检查是否就绪
  • 每次调用都要把fd集合从用户态拷贝到内核态,得到结果时都要把结果从内核态拷贝到用户态(这是所有系统调用避免不了的,内核态和用户态为了安全等考虑不会用同一个变量)
  • 同select一样,也会有用户层遍历整个辅助数组的消耗

多路转接之epoll

epoll的接口

epoll的原理

  • 当我们调用epoll_create创建一个epoll模型的时候,OS实际上就为我们创建了上述数据结构。而这个epoll模型也被关联到一个文件描述符上,从此以后,该epoll模型就被当作文件看待。
  • 当我们调用epoll_ctl添加,删除,修改要关心的fd及其事件,实际上是OS对红黑树进行增删改查,而红黑树就保存着用户要关心的所有fd及其事件。可以预见,用户不需要再去维护一个辅助数组,因为OS用红黑树帮我们维护了。
  • 除此之外,epoll模型中还管理着一个就绪队列,一但底层发现有事件就绪,就会把就绪的fd的epitem挂到就绪队列中。而每次用户调用epoll_wait都是从就绪队列中获取就绪的fd及其事件。
  • 当数据进入协议栈到达TCP/IP层的处理机制后,最后都会调用一个回调函数,但一般来说,这个回调函数是null,而当我们使用epoll模型关心一个fd后,该fd的回调函数就被注册。这个回调函数的功能就是:将红黑树的节点同时列入到就绪队列中,这样一来,OS不用轮询所有fd以检测是否就绪,因为fd就绪了就会自动通过回调函数列入到就绪队列。
    如果使用的是信号驱动IO,就把这个回调函数指向信号发送函数,这样一有事件就绪,就会自动通过回调函数发送SIGIO给对应进程,进程执行信号处理函数,而这个信号处理函数早已经被我们设置为执行读取或写入的代码了,这就是信号驱动IO的原理!!!

epoll模型 = 红黑树+就绪队列+回调机制

epoll的使用

  • 特别注意:
    如果不再关心某文件描述符上的事件并且还想要关闭文件描述符
    ,需要先调用epoll_ctl删除红黑树节点,然后再关闭文件描述符,不能颠倒顺序。因为调用epoll_ctl删除红黑树节点时,os会通过fd查找并修改一些属性(比如说把fd文件描述符对应的回调函数重新置为NULL),如果提前关闭了文件描述符这些内容早就销毁了。
  • 对于epoll来说,如果给epoll模型添加一个从未打开过的fd,会添加失败,如果在添加后fd关闭,那么epoll_wait可能会把这个fd失效的错误设置成一个就绪事件EPOLLERR返回给上层,而不是直接导致epoll_wait返回值小于0。

epoll的工作模式

LT(Level-triggered)

  • 是默认的模式
  • 事件(比如读就绪)被触发,如果用户没有读取完所有的内容,epoll就会一直通知,直到用户读完缓冲区所有的数据,如果读中间又来了数据,也得读完,否则就一直通知。
  • 本质上就是如果缓冲区仍然有数据,就一直把epitem挂在就绪队列里,这样每次epoll_wait都会返回它。

ET(Edge-triggered)

  • 调用epoll_ctl时设置,此设置只对单个文件描述符有效,也就是只有被设置的文件描述符是ET模式。
  • 事件(比如读就绪)被触发,epoll只在新数据到来时通知一次,无论用户是否读完,它也不在通知了。
  • 本质上就是一但该文件描述符对应的epitem节点被上层获取了一次,该节点就会立马从就绪队列被移除。
  • 使用ET模式可能丢失数据(比如最后一趟数据到来却没有读完,剩下的数据就不会再被读了),因此程序员要保证每一次收到通知都要读取完所有数据的。
    读取完所有数据的证明就是最近一次读取没有读到任何数据,但没读到数据会导致读函数阻塞,这会让整个进程都阻塞在这里。因此
    ET模式下fd要设置为非阻塞模式,用返回值+出错码来判断是否为空,为空就跳出函数,不为空继续读。

ET模式比LT模式更加高效

我们都学习过TCP中的流量控制,通信双方通过对方报文通告的窗口大小来决定这次要发送多少数据。在ET模式下,程序员必须保证每次读取数据的时候都把缓冲区的数据全部读完,因此几乎每次向对方通告窗口大小的时候都是整个缓冲区的大小,这样一来,对方发送数据的量几乎一直是最大的限度的量。直白的来讲,ET模式保证了双方通信的吞吐量变大,更好的利用网络带宽,因此ET模式的效率更高。

不过LT模式同样可以达到这个效果,我们只要在每次收到就绪通知的时候轮询读取完所有数据即可。这两种做法的唯一区别就是,ET模式是强制全部读取,否则就有可能丢失数据;而LT模式就算不全部读取也不会出错。

EPOLL的特点

对poll的补充:

  • 不需要在内核层遍历fd,就绪的事件通过回调函数自动列入就绪队列
  • 不需要用户遍历和维护辅助数组了,用户得到的一定是就绪的。

一个缺点:

  • 设置时要把epoll_event从用户态拷贝到内核态,得到结果时都要把它们从内核态拷贝到用户态(这是所有系统调用避免不了的,内核态和用户态为了安全等考虑不会用同一个变量),事实上,这不算缺点。

epoll囊括了select,poll的所有优点,同时也解决了他们几乎所有的缺点

使用poll编写简单的服务器

#include<iostream>
#include<unistd.h>
#include <sys/types.h>      
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/epoll.h>
#include <poll.h>
#include<string>
#include<string.h>
#include <strings.h>
using namespace std;

#define NUM 10

struct epoll_event fdarr[NUM];//这个数组用来接受就绪的fd结果集

void Accept(int fd,int epollfd)
{
   struct sockaddr_in addr;
   socklen_t len= sizeof(addr);
   bzero(&addr,len);
   //获取新连接
   int newfd = accept(fd,(struct sockaddr*)&addr,&len);
   
   cout<<"获取了新连接:"<<newfd<<endl;
   struct epoll_event event;
   bzero(&event,0);
   event.data.fd = newfd;
   event.events = EPOLLIN;
   epoll_ctl(epollfd,EPOLL_CTL_ADD,newfd,&event);
}

void Recv(int who,int epollfd)
{
   char buffer[1024] = {0};
   int n = recv(fdarr[who].data.fd,buffer,sizeof(buffer),0);
   if(n<0)
   {
      cout<<"读取出错.........."<<endl;
   }
   else if(n == 0)
   {
      cout<<"有一个用户退出........"<<endl;
      //这里一定要注意,在epoll模型中取消关心后在关闭fd
      epoll_ctl(epollfd,EPOLL_CTL_DEL,fdarr[who].data.fd,nullptr);
      close(fdarr[who].data.fd);
   }
   else
   {
      buffer[n] = 0;
      cout<<"收到一条信息:"<<buffer<<endl;
      string message = "echo#";
      message+=buffer;
      send(fdarr[who].data.fd,message.c_str(),message.size(),0);
   }
}


void Dispatcher(int epollfd,int listenfd,int count)//count表示就绪的fd的数量
{
   //遍历fdarr,它们都是就绪的
   for(int i = 0;i<count;i++)
   {
      if((fdarr[i].events & EPOLLIN) > 0)
      {
         //监听套接字就绪
         if(fdarr[i].data.fd == listenfd)
         {
            Accept(fdarr[i].data.fd,epollfd);
         }
         //普通套接字的就绪
         else
         {
            Recv(i,epollfd);
         }
      }
      //这个位置有fd并且fd写就绪了
      else
      {
         //......
      }
    
   }
}


int main(int argc,char * argv[])
{
   if(argc<2)
   {
      cout<<"请给出端口号"<<endl;
      return 0;
   }
   //创建用于监听的套接字
   int listen_fd = socket(AF_INET,SOCK_STREAM,0);

   //创建套接字的标识信息结构体
   struct sockaddr_in addr;
   bzero(&addr,sizeof(addr));
   addr.sin_family = AF_INET;
   addr.sin_port = htons(stoi(argv[1]));
   addr.sin_addr.s_addr = INADDR_ANY;

   //设置地址重用
   int reuse = 1;
   setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

   //将标识信息结构体绑定到套接字上
   bind(listen_fd,(struct sockaddr*)&addr,sizeof(addr));

   //开启监听
   listen(listen_fd,5);

   //创建epoll模型
   int epoll_fd = epoll_create(1);

   //监听套接字获取链接也是一种IO,因此把监听套接字加入epoll
   struct epoll_event event;
   bzero(&event,0);
   event.data.fd = listen_fd;
   event.events = EPOLLIN;
   epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,&event);

   while(1)
   {
      int ret = epoll_wait(epoll_fd,fdarr,NUM,1000);
      if(ret>0)
      {
         //提取就绪的事件
         Dispatcher(epoll_fd,listen_fd,ret);
      }
      else if(ret == 0)
      {
         cout<<"没有事件就绪"<<endl;
      }
      else
      {
         cout<<"poll error"<<endl;
      }
   }

}

后记

介绍了这么多,实际上,我想说的就是多路转接的几种实现方式,因为多路转接可以提高IO效率。但是我们可以发现,只是用单进程多路转接来实现一个服务器,虽然IO效率高,但是服务器运行效率不高,因为服务器是一个个的处理各种请求的,如果活跃的链接较多这会让后面的用户等待时间较长,体验感不好,即服务器的运行效率太低。

因此在实际工程中会使用多Reactor来解决这种问题

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐