五大IO模型

Posted ych9527

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了五大IO模型相关的知识,希望对你有一定的参考价值。

1.IO过程

任何IO过程,都要包含两个步骤,第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少

等待IO就绪:相当于想要获取的资源已经准备好了,可以进行操作

读 recv/recvfrom: 等待接收缓冲区当中有数据来(等待IO过程)、接收缓冲区之中有了数据(等待IO就绪)
写 send/sendto:发送缓冲区当中有空间(等待IO过程)、发送缓冲区之中有了空间(等待IO就绪)

拷贝数据到缓冲区中:
读: recv/recvfrom(sockfd,buff,size,0): 将接收缓冲区的数据拷贝到自己准备的空间(buff)之中
写: send/sendto:将应用层数据,拷贝到发送缓冲区之中

2.典型IO模型

2.1阻塞IO

资源不可用的情况下,IO请求一直被阻塞,直到资源可用,就叫做阻塞IO,所有的套接字, 默认都是阻塞方式,阻塞IO是最常见的IO模型

实现流程:
在这里插入图片描述

特点:

1.发起IO调用后,等待的时间取决于内核

2.在等待的过程之中,执行流是被挂起的,对CPU的利用率是非常低

3.在IO就绪到拷贝数据之间,实时性是非常高的(有鱼咬立马起杠提钩)

2.2非阻塞IO

资源不可用的时候,IO请求不会阻塞,而是直接返回,返回当前资源不可用,并且返回EWOULDBLOCK错误码

如果当前资源不可用,IO请求返回之后,表示本次IO请求没有真正完成,所以想要完成IO请求,非阻塞需要搭配循环使用,直至完成IO请求

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

实现流程:

在这里插入图片描述

特点:

1.非阻塞IO对CPU的利用率比阻塞IO高

2.代码复杂,流程控制复杂,因为需要循环的缘故

3.需要搭配循环调用,直至IO请求完成

4.IO准备就绪到拷贝数据之间不够实时

while{
非阻塞IO调用  //资源还未准备好,返回错误码
此时资源已经准备就绪,但是还需要指向下面代码,因此实时性比较差
//代码1
//代码2
}

区分阻塞IO和非阻塞IO:只需要关心IO调用是否立即返回即可,没有立即返回说明是阻塞的,直接返回说明是非阻塞的

2.3信号驱动IO

实现流程:

1.自定义一个IO信号(SIGIO)的处理函数,在处理函数当中发起IO调用

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

3.程序收到一个IO信号,内核就会调用自定义的处理函数,内核调用了自定义的处理函数,在自定义处理函数中发起了IO调用

在这里插入图片描述

特点:

1.IO准备就绪,到拷贝数据之间,实时性增强了

2.代码更加复杂,流程控制更加困难,因为引入了信号

3.好处是不用再重复发起IO调用,但是需要在代码当中增加自定义信号的逻辑

2.4异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

实现流程:

1.自定义信号处理函数 -> 通知数据拷贝完成

2.发起一个异步IO调用,并且异步IO调用直接返回

3.异步IO调用返回之后,执行流可以执行用户代码,由操作系统内核等待IO就绪和数据拷贝

4.数据拷贝完成之后,内核通过信号通知调用者
在这里插入图片描述

3.多路转接IO模型

作用:IO多路转接可以完成大量文件描述符的监控,监控的时间包括:可读事件、可写事件、异常事件

监控文件描述符:那么个文件描述符就绪,就处理哪一个文件描述符

3.1select模型

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

实现流程:

1.将用户关心的文件描述符拷贝到内核之中,由内核进行监控

2.如果内核监控到某个文件描述符就绪,则返回该描述符

3.用户针对返回的描述符进行操作

接口:
在这里插入图片描述

代码演示1:不带超时时间

int main()
  7 {
  8   //设置事件集合
  9   fd_set readfds;//
 10 
 11   FD_ZERO(&readfds);//清空集合
 12   FD_SET(0,&readfds);//将0号文件描述符添加进去
 13 
 14   while(1)
 15   {
 16       int ret=select(1,&readfds,NULL,NULL,NULL);//阻塞监控
 17     if(ret < 0)
 18     {
 19       cerr<<"select error"<<endl;//监控出错
 20       exit(1);
 21     }
 22 
 23     if(FD_ISSET(0,&readfds)!=0)//判断0号文件描述符是否准备就绪
 24     {
 25       char buff[100]={0};
 26       read(0,buff,sizeof(buff)-1);
 27       printf("echo:%s",buff);
 28     }
 29   }
 30 
 31   return 0;
 32 }                   

在这里插入图片描述

代码演示2:带超时时间

int main()
{
  //设置事件集合
  fd_set readfds;//
  
  FD_ZERO(&readfds);//清空集合
  FD_SET(0,&readfds);//将0号文件描述符添加进去

  while(1)
  {
    //设置超时时间
    struct timeval tv;
    tv.tv_sec=3;//超时时间设置为3秒
    tv.tv_usec=0;

    int ret=select(1,&readfds,NULL,NULL,&tv);//阻塞监控

    if(ret < 0)
    {
      cerr<<"select error"<<endl;//监控出错
      exit(1);
    }

    if(ret==0)//超时了
    {
      cout<<"time out"<<endl;

      if(FD_ISSET(0,&readfds)==0)//超时了,select会去除没有就绪的文件描述符
        cout<<" 0 fd is not in readfds"<<endl;

      FD_SET(0,&readfds);//从新设置
      continue;
    }

    if(FD_ISSET(0,&readfds)!=0)//判断0号文件描述符是否准备就绪
    {
      char buff[100]={0};
      read(0,buff,sizeof(buff)-1);
      printf("echo:%s",buff);
    }
  }

  return 0;
}

在这里插入图片描述

select优缺点:

优点:
1.遵循的是posix标准,即可以跨平台使用
2.select超时时间可以精确到微秒

缺点:
1.select采用轮询遍历,监控的效率会随着文件描述符的增多而下降
2.select所能监控的文件描述符是有上限的(1024),取决于内核的FD_SETSIZE宏值
3.select监控文件描述符的时候,需要将集合拷贝到内核之中,select发现有事件就绪之后(返回值大于0,表示发现),同时需要将事件从内核拷贝到用户空间,效率也会受到印象
4.select在返回的时候,会将未就绪的文件描述符从集合中去除,导致下一次监控的时候,如果还需要监控去掉的文件描述符,就需要重新进行添加
5.select没有直接告诉程序员哪一个文件描述符就绪了,需要程序员在返回的事件集合当中去判断

用select构建tcp服务器

构建流程:

在这里插入图片描述

构建代码:

#pragma once
#include <sys/select.h>
#include <iostream>
using namespace std;
#include <math.h>
#include <vector>

class Select
{
  public:

    Select()
    {
      //初始化事件集合、最大文件描述符
      FD_ZERO(&readfds);
      _maxfd=-1;
    }
    

    void AddSet(int fd)//添加需要监控文件描述符
    {
      _maxfd=fmax(_maxfd,fd);//更新最大文件描述符
      FD_SET(fd,&readfds);

    }

    void DeleteSet(int fd)//删除监控的文件描述符
    {
      FD_CLR(fd,&readfds);
      
      //更新最大文件描述符
      if(fd==_maxfd)
      {
        for(int i=fd;i>=0;i--)//从后往前寻找第一个就是最大的
        {
          if(FD_ISSET(i,&readfds))
          {
            _maxfd=i;
            break;
          }
        }
      }
    }
    
    bool SelectWait(vector<int>& arr)//监控接口
    {
      //设置延迟时间
      struct timeval tv;
      tv.tv_sec=2;
      tv.tv_usec=0;
      
      fd_set copy = readfds;//保存一份,返回后可以进行恢复

      int ret=select(_maxfd+1,&copy,NULL,NULL,NULL);
      
      if(ret < 0)//监控错误
      {
        cerr<<"select error"<<endl;
        return false;
      }
      else if(ret==0)//等待超时了
      {
        cout<<"time out"<<endl;
        return false;
      }

      //监控了多个文件描述符,但是不知道那个文件描述符已经就绪了
      for(int i=0;i<=_maxfd;i++)
      {
        if(FD_ISSET(i,&copy))//为i的文件描述符,已经就绪了
            arr.push_back(i);//添加到准备就绪的集合之中
      }
        return true;
    }


    ~Select()
    {}



  private:
    int _maxfd;//最大的文件描述符
    fd_set readfds;//可读事件集合
};

#ifndef _SERVER_HPP_
#define _SERVER_HPP_

#include <iostream>
using namespace std;
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#endif
#define BACKLOCK 5

class Server
{
  private:
    int port;
    int lsock;//监听套接字
  
  public:
    
    Server(int _port)
      :port(_port)
       ,lsock(-1)
    {}

    void Init()//初始化服务器
    {
      lsock=socket(AF_INET,SOCK_STREAM,0);
      if(lsock<0)
      {
        cerr<<"socket fail"<<endl;
        exit(2);
      }
      
      struct sockaddr_in local;
      local.sin_family=AF_INET;//填充协议
      local.sin_port=htons(port);//填充端口,转换成网网络字节序
      local.sin_addr.s_addr=INADDR_ANY;//填写ip
    
      if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)//进行绑定
      {
        cerr<<"bind error"<<endl;
        exit(3);
      }
      if(listen(lsock,BACKLOCK) < 0)
      {
        cerr<<"listen error"<<endl;
        exit(4);
      }
    }

    int Task(int sock)//用链接的套接字去执行任务
    {
      char buff[200];
        size_t size=recv (sock,buff,sizeof(buff)-1,0);
        if(size>0)//读取到了
        {
          buff[size]='\\0';//末尾添加\\0
          cout<<"client:"<<buff<<endl;

          string str="ser echo:";
          str+=buff;
          send(sock,str.c_str(),str.size(),0);//不需要+1,\\0是语言的标准,文件不认识
          
          return 0;
        }
        else 
          return -1;//表示对方已经关闭了
    }

    int Stat()//用套接字去获取链接
    {
      sockaddr_in end_point;//获取客户结构体信息,和udp作用一样
        socklen_t len=sizeof(end_point);
        int sock=accept(lsock,(struct sockaddr*)&end_point,&len);
        
        if(sock < 0)
        {
          cout<<"accept error"<<endl;
            return -1;
        }
        
        return sock;
    }

    int GetPid()//返回监听套接字
    {
      return lsock;
    }



  ~Server()
  {
	  close(lsock);
  }
};

#include "select.hpp"
#include "server.hpp"


int main(int argc,char *argv[])
{
  if(argc!=2)
  {
    cout<<"please enter your port"<<endl;
    exit(0);
  }

  Server se(atoi(argv[1]));
  se.Init();
  
  Select st;
  st.AddSet(se.GetPid());

  while(1)
  {
    vector<int> arr;
    bool ret=st.SelectWait(arr);//进行监控
    
    if(ret==false)
        continue;
    
    for(size_t i=0;i < arr.size();i++)
    {
      if(arr[i]==se.GetPid())
      {
          int sock=se.Stat();//获取链接
          
          if(sock>0)
          {
            st.AddSet(sock);//将获取的链接添加至监控中
            cout<<"get a new link"<<endl;
          }
      }
      else
      {
        int ret=se.Task(arr[i]);
        if(ret==-1)//对方已经关掉了
        {
          st.DeleteSet(arr[i]);
          cout<<"link end"<<endl;
        }
      }
    }
  }
  return 0;
}

3.2poll模型

接口:

在这里插入图片描述

特点:

1.poll和select相比,跨平台移植性不如select,与epoll相比,监控效率不如epoll

2.相较于select改进的点:

a.不限制文件描述符的个数了
b.相较于select之前的事件集合的方式,改进成为事件结构。事件结构告诉我们,关心的文件描述符是什么,关心的文件描述符发生事件是什么

代码验证:

int main()
{
  struct pollfd arr[10];
  arr[0].fd=0;//关心0号文件描述符
  arr[0].events=POLLIN;//关心可读事件

  while(1)
  {
    int ret=poll(arr,1,2000);//1个有效元素,超时时间为2000毫秒
    if(ret==0)//等待超时
    {
      cout<<"time out"<<endl;
      continue;
    }
    else if(ret < 0)
    {
      cerr<<"poll error"<<endl;//poll失败
   	  exit(0);
    }
    else
    {
      char buff[100];
      for(int i=0;i<ret;i++)
      {
        if(arr[i].events==POLLIN)
        {
          int size=read(arr[i].fd,buff,sizeof(buff)-1);
          buff[size-1]=0;//会将换行符也读进来
          cout<<"echo:"<<buff<<endl;
         }
      }
    }
}

  return 0;

}

在这里插入图片描述

优缺点:

优点:
1.采用了事件结构的方式,简化了代码的编写
2.不限制文件描述符的个数
3.不需要在二次监控的时候重新添加文件描述符

缺点:
1.采用轮询遍历事件结构数组的方式,随着文件描述符增多,性能下降
2.不支持跨平台
3.也没有告诉用户哪一个具体的文件描述符就绪了,需要程序员信息遍历判断
4.也需要将事件结构拷贝到内核,再从内核拷贝用户空间

3.3epoll模型

3.3.1epoll的使用介绍

目前公认的在linux操作系统下,监控性能最高的

接口:

epoll_create:
在这里插入图片描述

epoll_ctl:
在这里插入图片描述

epoll_wait:

在这里插入图片描述

epoll原理:

在这里插入图片描述

使用epoll构建tcp服务器

构建流程:

在这里插入图片描述

构建代码:

#pragma once
#include <iostream>
using namespace  std;
#include <unistd.h>
#include <sys/epoll.h>
#include <vector>

class EpollSvr
{

public:
  EpollSvr()
    :epfd(-1)
  {}

  void EpollIinit()
  {
    //进行创建,返回的是操作句柄
    epfd=epoll_create(10);
    if(epfd < 0)
    {
      cout<<"EpollINIT Error"<<endl;
      exit(0);
    }
  }

  void EpollAdd(int fd)
  {
    //创建事件结构体
    struct epoll_event ep;
    ep.events=EPOLLIN;//关心的事件(可读)
    ep.data.fd=fd;//告诉epoll,关心的文件描述符

    int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ep);//添加关心的文件名描述符fd
    
    if(ret < 0)//添加失败
    {
      cout<<"epoll add error"<<endl;
      exit(1);
    }
  }

  void EpollDelete(int fd)
  {
    int ret=epoll_ctl(epfd,EPOLL_CTL_DEL,fd,nullptr);//删除文件描述符
    
    if(ret < 0)//删除失败
    {
      cout<<"epoll delete error"<<endl;
      exit(2);
    }
  }

  bool EpollWait(vector<int> &out)
  {
    struct epoll_event  arr[10];//定义事件结构体数组
    int size=sizeof(arr)/sizeof(arr[0]);

    int ret=epoll_wait(epfd,arr,size,-1);//超时时间设置小于0,是阻塞等待

    if(ret < 0)
        return false;

    if(ret > size)//判断事件是否超过数组大小
      ret = size;

    for(int i=0;i < ret; i++)
    {
      out.push_back(arr[i].data.fd);//将就绪的文件描述符进行返回
    }

    return true;
  }

  ~EpollSvr()
  {}

  private:
    int epfd;
};
#include <iostream>
using namespace std;
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#endif
#define BACKLOCK 5

class Server
{
  private:
    int port;
    int lsock;//监听套接字
  
  public:
    
    Server(int _port)
      :port(_port)
       ,lsock(-1)
    {}

    void Init()//初始化服务器
    {
      lsock=socket(AF_INET,SOCK_STREAM,0);
      if(lsock < 0)
      {
        cerr<<"socket fail"<<endl;
        exit(2);
      }
      
      struct sockaddr_in local;
      local.sin_family=AF_INET;//填充协议
      local.sin_port=htons(port);//填充端口,转换成网网络字节序
      local.sin_addr.s_addr=INADDR_ANY;//填写ip
    
      if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)//进行绑定
      {
        cerr<<"bind error"<<endl;
        exit(3);
      }
      if(listen(lsock,BACKLOCK) < 0)
      {
        cerr<<"listen error"<<endl;
        exit(4);
      }
    }

    int Task(int sock)//用链接的套接字去执行任务
    {
      char buff[200];
        size_t size=recv (sock,buff,sizeof(buff)-1,0);
        if(size>0)//读取到了
        {
          buff[size]='\\0';//末尾添加\\0
          cout<<"client:"<<buff<<endl;

          string str="ser echo:";
          str+=buff;
          send(sock,str.c_str(),str.size(),0);//不需要+1,\\0是语言的标准,文件不认识
          return 0;
        }
        else 
        {
          //close(sock);
          return -1;//表示对方已经关闭了
        }
    }

    int Stat()//用套接字去获取链接
    {
      sockaddr_in end_point;//获取客户结构体信息,和udp作用一样
        socklen_t len=sizeof(end_point);
        int sock=accept(lsock,(struct sockaddr*)&end_point,&len);
        
        if(sock < 0)
        {
          cout<<"accept error"<<endl;
            return -1;
        }
        
        return sock;
    }

    int GetPid()//返回监听套接字
    {
      return lsock;
    }



  ~Server()
  {
	  close(lsock);
  }
};
#include "EpollSvr.hpp"
#include "server.hpp"


int main(int argc,char *argv[])
{
  if(argc!=2)
  {
    cout<<"please enter your port"<<endl;
    exit(0);
  }

  Server se(atoi(argv[1]));
  se.Init();
  
  EpollSvr es;
  es.EpollIinit();//初始化
  es.EpollAdd(se.GetPid());//将侦听套接字添加进去

  while(1)
  {
    vector<int> arr;
    bool ret= es.EpollWait(arr);//进行监控
    if(ret==false)
        continue;
    
    for(size_t i=0;i < arr.size();i++)
    {
      if(arr[i]==se.GetPid())
      {
          int sock=se.Stat();//获取链接
          
          if(sock>0)
          {
            es.EpollAdd(sock);//将获取的链接添加至监控中
            cout<<"get a new link"<<endl;
          }
      }
      else
      {
        int ret=se.Task(arr[i]);
        if(ret==-1)//对方已经关掉了
        {
          es.EpollDelete(arr[i]);//将文件描述符进行删除
          cout<<"link end"<<endl;
          close(arr[i]);
        }
      }
    }


  }
  return 0;
}

3.3.2poll对文件描述符就绪事件的触发方式

3.3.2.1水平触发EPOLLLT

满足条件就会一直触发 -> 比如你在打游戏,你的妈妈叫你去吃饭,你没去,她又会过来叫你
EPOLLLT -> epoll的默认工作方式,select和poll都是水平触发方式

可读事件:

只要接收缓冲区当中的数据大于低水位标记(1字节),就会一直触发可读事件,直到接收缓冲区当中没有数据可读(接收缓冲区当中的数据低于低水位标记)

可写事件:

只要发送缓冲区当中的空间大于低水位标记(1字节),就会一直触发可写事件,直到发送缓冲区当中没有空间可写(发送缓冲区当中的空间低于低水位标记)

3.3.2.1边缘触发(边沿触发)EPOLLET

满足条件后只会触发一次 -> 比如你在打游戏,你的爸爸叫你去吃饭,只会叫你一次

EPOLLET -> 只有epoll才拥有

设置:
设置文件描述符对应的事件结构的时候,只需要在事件结构当中的事件变量中按位或上EPOLLET即可

struct epoll_event et; 
ev.events = EPOLLIN|EPOLLET;

可读事件:

只有当新的数据到来的时候,才会触发可读,否则通知一次之后,就不再通知了 -> 每次到来一个新的数据,只会通知一次,如果应用程序没有将接收缓冲区的数据读完(没有读完的数据留在缓冲区之中,下次触发就从这里开始),也不会再次通知,直到新的数据到来,才会触发可读事件,因此需要尽量将数据读完

可写事件:

只有当发送缓冲区之中剩余空间从不可写变成可写的时候,才会触发一次可写事件就绪

对于ET模式而言,如果就绪事件产生,一定要把握好机会,对于可读事件,将数据读完,对于可写事件,将数据写完
ET模式结合了循环将数据进行读取和发送,不是频繁的进行通知,因此效率就比较高

使用ET模式对代码进行修改:

构建细节注意点:

1.如何判断数据读完了 :

设size=期望读取的字节、ret为实际读取的字节
ret<size表示缓冲区之中一定没有数据了 -> 读完了

ret==size
此时有可能有数据,有可能没有 ->都需要再次进行读取
再次读取有可能会进入阻塞,因此需要将其更改为非阻塞状态

非阻塞状态下,读空的时候,会报错并且返回(EAGAIN/EWOULDBLOCK),因此需要对异常进行特殊的处理

2.将数据发送出去:

同样需要构建循环进行发送,当缓冲区没有容量的时候,就循环发送,直至缓冲区有容量

3.将描述符设置为非阻塞接口介绍:

int fcntl(int fd, int cmd, … /* arg */ );
1.fd:要设置的文件描述符
2.cmd:操作方式
F_GETFL获取当前文件描述符的属性
F_SETFL将非阻塞属性设置到文件描述符的属性当中(O_NONBLOCK)

4.代码

修改细节:
1.对侦听套接字进行监控时,采用默认的方式
2.对链接的套接字进行监控时,采用ET模式
3.由于是ET模式,应该通知的时候就需要将所有的数据读取或者发送完毕,因此需要采用循环来处理
4.循环处理中,如果是阻塞的发送,那么最后一次处理会陷入阻塞的状态,因此需要改为非阻塞的状态
5.非阻塞状态中,返回值小于0的时候,有可能会报异常,因此需要特殊处理
#pragma once
#include <iostream>
using namespace  std;
#include <unistd.h>
#include <sys/epoll.h>
#include <vector>

class EpollSvr
{

public:
  EpollSvr()
    :epfd(-1)
  {}

  void EpollIinit()
  {
    //进行创建,返回的是操作句柄
    epfd=epoll_create(10);
    if(epfd < 0)
    {
      cout<<"EpollINIT Error"<<endl;
      exit(0);
    }
  }

  void EpollAdd(int fd,bool is_et=false)
  {
    //创建事件结构体
    struct epoll_event ep;

    if(is_et)//设置为ET模式
      ep.events=EPOLLIN|EPOLLET;//关心的事件(可读)
    else
      ep.events=EPOLLIN;

    ep.data.fd=fd;//告诉epoll,关心的文件描述符

    int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ep);//添加关心的文件名描述符fd
    
    if(ret < 0)//添加失败
    {
      cout<<"epoll add error"<<endl;
      exit(1);
    }
  }

  void EpollDelete(int fd)
  {
    int ret=epoll_ctl(epfd,EPOLL_CTL_DEL,fd,nullptr);//删除文件描述符
    
    if(ret < 0)//删除失败
    {
      cout<<"epoll delete error"<<endl;
      exit(2);
    }
  }

  bool EpollWait(vector<int> &out)
  {
    struct epoll_event  arr[10];//定义事件结构体数组
    int size=sizeof(arr)/sizeof(arr[0]);

    int ret=epoll_wait(epfd,arr,size,-1);//超时时间设置小于0,是阻塞等待

    if(ret < 0)
        return false;

    if(ret > size)//判断事件是否超过数组大小
      ret = size;

    for(int i=0;i < ret; i++)
    {
      out.push_back(arr[i].data.fd);//将就绪的文件描述符进行返回
    }

    return true;
  }

  ~EpollSvr()
  {}

  private:
    int epfd;
};

#ifndef _SERVER_HPP_
#define _SERVER_HPP_

#include <iostream>
using namespace std;
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <fcntl.h>
#endif
#define BACKLOCK 5

class Server
{
  private:
    int port;
    int lsock;//监听套接字
  
  public:
    
    Server(int _port)
      :port(_port)
       ,lsock(-1)
    {}

    void Init()//初始化服务器
    {
      lsock=socket(AF_INET,SOCK_STREAM,0);
      if(lsock < 0)
      {
        cerr<<"socket fail"<<endl;
        exit(2);
      }
      
      struct sockaddr_in local;
      local.sin_family=AF_INET;//填充协议
      local.sin_port=htons(port);//填充端口,转换成网网络字节序
      local.sin_addr.s_addr=INADDR_ANY;//填写ip
    
      if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)//进行绑定
      {
        cerr<<"bind error"<<endl;
        exit(3);
      }
      if(listen(lsock,BACKLOCK) < 0)
      {
        cerr<<"listen error"<<endl;
        exit(4);
      }
    }
    

    void SetNonBlock(int sock)//设置为非阻塞状态
    {
      int flag=fcntl(sock,F_GETFL);//获取当前文件描述符属性
      fcntl(sock,F_SETFL,flag | O_NONBLOCK);//将文件描述符设置为非阻塞
    }


    int Task(int sock)//用链接的套接字去执行任务
    {
        string message; 
        while(1)
        {
          //进行数据的读取 
          
          //设置读取缓冲区
          char buff[3]={0};
          int num=recv(sock,buff,sizeof(buff)-1,0);//实际读取到多少数据
          
          if(num < 0)
          {
            if(errno == EAGAIN || errno == EWOULDBLOCK)//没有数据了,再进行读取出现报错
                break;
            
            cerr<<"recv error"<<endl;
            return -1;
          }
          else if(num==0)//对端关闭了链接
          {
            return -1;
          }
        
          message+=buff;//将上次结果进行累加

          if(num < sizeof(buff)-1)//全部读取完毕
              break;
        }

        cout<<"client:"<<message;

        //进行数据的发送
        //回显字符串
        string send_str("echo:");
        send_str+=message;
        
        size_t pos=0;//发送位置
        size_t send_size=send_str.size();//剩余发送字符的数量

        while(1)
        {
          size_t size=send(sock,send_str.c_str()+pos,send_size,0);//从什么位置发送多少数据
          if(size < 0)
          {
            if(errno == EAGAIN || errno == EWOULDBLOCK)//空间满了,继续发送
                continue;

            //出错了,对方关闭了链接
            cerr<<"send error"<<endl;
            return -1;
          }

          //更新下一次发送数据的位置,和剩余需要发送的数据
          pos+=size;
          send_size-=size;

          if(send_size <= 0)//发送完毕了
            break;
        }

        return 1;
    }


    int Stat()//用套接字去获取链接
    {
      sockaddr_in end_point;//获取客户结构体信息,和udp作用一样
        socklen_t len=sizeof(end_point);
        int sock=accept(lsock,(struct sockaddr*)&end_point,&len);
        
        if(sock < 0)
        {
          cout<<"accept error"<<endl;
            return -1;
        }
        
        return sock;
    }

    int GetPid()//返回监听套接字
    {
      return lsock;
    }



  ~Server()
  {
	  close(lsock);
  }
};

#include "EpollSvr.hpp"
#include "server.hpp"

int main(int argc,char *argv[])
{
  if(argc!=2)
  {
    cout<<"please enter your port"<<endl;
    exit(0);
  }

  Server se(atoi(argv[1]));
  se.Init();
  
  EpollSvr es;
  es.EpollIinit();//初始化
  es.EpollAdd(se.GetPid());//将侦听套接字添加进去

  while(1)
  {
    vector<int> arr;
    bool ret= es.EpollWait(arr);//进行监控
    if(ret==false)
        continue;
    
    for(size_t i=0;i < arr.size();i++)
    {
      if(arr[i]==se.GetPid())
      {
          int sock=se.Stat();//获取链接
          
          if(sock>0)
          {
            //se.SetNonBlock(sock);//将套接字设置为非阻塞状态

            es.EpollAdd(sock,true);//将获取的链接添加至监控中,并且是ET模式
            cout<<"get a new link"<<endl;
          }
      }
      else
      {
        se.SetNonBlock(arr[i]);//将套接字设置为非阻塞状态
        int ret=se.Task(arr[i]);

        if(ret==-1)//对方已经关掉了
        {
          es.EpollDelete(arr[i]);//将文件描述符进行删除
          cout<<"link end"<<endl;
          close(arr[i]);
        }
      }
    }


  }
  
  return 0;
}

以上是关于五大IO模型的主要内容,如果未能解决你的问题,请参考以下文章

真正涨知识了!Linux五大网络IO模型详解

IO模型及高性能网络架构分析

java内存流:java.io.ByteArrayInputStreamjava.io.ByteArrayOutputStreamjava.io.CharArrayReaderjava.io(代码片段

java缓冲字符字节输入输出流:java.io.BufferedReaderjava.io.BufferedWriterjava.io.BufferedInputStreamjava.io.(代码片段

稳定性 耗时 监控原因分析-- dubbo rpc 框架 的线程池,io 连接模型. 客户端,服务端

十大视频场景化应用工具+五大视频领域冠军/顶会算法开源