Linux ----高级IO

高级IO

在这里插入图片描述

五种IO模型

任何IO过程中,都包含两个步骤:1、等待2、拷贝。在实际的应用场景中,等待消耗的时间往往都远高于拷贝时间,让IO更高效,最核心的办法是让等待的时间尽量少

阻塞IO

在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式
阻塞IO是最常见的IO模型:
在这里插入图片描述
所谓阻塞,是用户层的感受,在内核中本质是进程被挂起

非阻塞IO

如果内核还未准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。
在这里插入图片描述

非阻塞IO需要通过循环的方式反复尝试读取文件描述符,这个过程称之为轮询,这对CPU来说是较大的浪费,一般只有在特定场景下才会使用。
非阻塞轮询的本质是在做事件就绪(底层OS有数据)的检测工作

信号驱动IO

内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
在这里插入图片描述

IO多路转接

虽然从流程图上看与阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
在这里插入图片描述

异步IO

由内核在数据拷贝完成时,通知应用程序(信号驱动是告诉应用进程何时可以考试拷贝数据)
在这里插入图片描述

高级IO概念

同步通信 vs 异步通信

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

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是,一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用结果
  • 异步是调用发出后,该调用就直接返回,没有调用结果。换句话说,就是当一个异步过程调用发出后,调用者不会立即得到结果,而是在调用发出后,被调用者通过状态,通知调用者,或者通过回溯函数处理这个调用

多线程的同步和互斥

  • 进程/线程同步也是进程/线程之间的直接制约关系
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协它们的工作次序而等待、传递信息所产生的制约关系,尤其是在访问临界资源的时候

阻塞 vs 非阻塞

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

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

其他高级IO

非阻塞IO,记录锁,,系统V流机制,I/O多路转接(I/O多路复用),readv和writev函数以及存储映射IO(mmap)

非阻塞IO

fcntl

一个文件描述符,默认都是阻塞IO

 #include <unistd.h>
#include <fcntl.h>

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,SETLK/F_SETLKW)

将获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞

实现函数SetNoBlock

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

void SetNoBlock(int fd)
{
	int fl = fcntl(fd,F_GETFL);
	if(fl < 0)
	{
		perror("fcntl");
		return;
	}
	fcntl(fd,F_SETFL,fl | O_NONBLOCK);
}
  • 使用F_GETFL将当前文件描述符的属性取出来(是位图)
  • 使用F_SETFL将文件描述符设置回去,设置回去的同时加上O_NONBLOCK参数。
以轮询方式读取数据
#include <iostream>
#include <unistd.h>
#include <fcntl.h>

bool  SetNoBlock(int fd)
{
	int fl = fcntl(fd,F_GETFL);
	if(fl < 0)
	{
    std::cerr << "fcntl error" << std::endl;
		return false;
	}
	fcntl(fd,F_SETFL,fl | O_NONBLOCK);
  return true;
}
#define NUM 1024
int main()
{
  SetNoBlock(0);
  while(true)
  {
    char buffer[NUM];
    ssize_t size = read(0,buffer,sizeof(buffer) - 1);
    if(size < 0)
    {
      //如果非阻塞,读取数据时,没有就绪,read以出错的形式进行返回
      //不一定是出错的有可能是底层没有数据
      if(errno == EAGAIN || errno == EWOULDBLOCK)
      {
        std::cout << "底层的数据没有就绪,轮询检测"<<std::endl;
        sleep(1);
      }
      std::cerr << "read error "<< size << " errno:"<< errno << std::endl;
      continue;
    }
    buffer[size] = 0;
    std::cout << "echo# "<< buffer << std::endl;
  }
  return 0;
}

在这里插入图片描述

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
bool  SetNoBlock(int fd)
{
	int fl = fcntl(fd,F_GETFL);
	if(fl < 0)
	{
    std::cerr << "fcntl error" << std::endl;
		return false;
	}
	fcntl(fd,F_SETFL,fl | O_NONBLOCK);
  return true;
}
void handler(int signo)
{
  std::cout << "get a signo" << std::endl;
}
#define NUM 1024
int main()
{
  //SetNoBlock(0);
  signal(2,handler);
  while(true)
  {
    std::cout<<"do something"<<std::endl;
    char buffer[NUM];
    ssize_t size = read(0,buffer,sizeof(buffer) - 1);
    if(size < 0)
    {
      //如果非阻塞,读取数据时,没有就绪,read以出错的形式进行返回
      //不一定是出错的有可能是底层没有数据
      if(errno == EAGAIN || errno == EWOULDBLOCK)
      {
        std::cout << "底层的数据没有就绪,轮询检测"<<std::endl;
        sleep(1);
        continue;
      }
      if(errno == EINTR)
      {
        std::cout << "底层数据就绪未知,被信号中断"<<std::endl;
        continue;
      }
      else 
      {
        std::cerr << "read error "<< size << " errno:"<< errno << std::endl;
        break;
      }
      continue;
    }
    buffer[size] = 0;
    std::cout << "echo# "<< buffer << std::endl;
  }
  return 0;
}

在这里插入图片描述

I/O多路转接

select

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

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

select函数

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
  • 参数nfds是需要监视的最大的文件描述符值+1,对多个文件描述符进行轮询检测
  • fd_set 类似于sigset_t 是一个位图,可以将特定的fd添加到位图中,是一个具体的数据类型(大小确定)
  • readfds,writefds,exceptfds分别对应需要检测的可读文件描述符的集合,可写文件描述符的集合,以及异常文件描述符的集合
  • 参数timeout为结构timeval,用来设置select的等待时间
timeout
  • NULL:表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件
  • 0:仅检测描述符集的状态,然后立即返回,并不等待外部事件的发生
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回
timeval

           struct timeval {
               long    tv_sec;         /* seconds */
               long    tv_usec;        /* microseconds */
           };

   and
           struct timespec {
               long    tv_sec;         /* seconds */
               long    tv_nsec;        /* nanoseconds */
           };

timeval结构用来描述一段时间长度,如果在这个时间之内,需要监视的描述符没有事件发生则函数返回,返回值为0

函数返回值
  • 执行成功则返回文件描述符词状态已改变的个数
  • 返回0表示在描述词状态改变之前以超过timeout时间,没有返回
  • 有错误返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值不可预测
错误值
  • EBADF:文件描述词为无效的或该文件已经关闭
  • EINTR:此调用被信号中断
  • EINVAL:参数n为负值
  • ENOMEM:核心内存不足
fd_set结构

是一个整数数组/位图,使用位图中对应的位来表示要监视的文件描述符

fd_set接口

  • void FD_CLR(int fd,fd_set *set)//用来清除描述词组set中相关fd的位
  • void 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中全部位

所有的fd_set既是输入型也是输出型
在这里插入图片描述
fd_set大小是1024所以select能够同时等待的文件描述符是有上限的

select函数工作流程

在这里插入图片描述

select就绪条件

读就绪

  • socket内核中,接收缓冲区中的字节数大于等于低水平位标记SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
  • socket TCP通信中,对端连接关闭,此时对socket读,返回0;
  • 监听socket上有新的连接请求
  • socket上有未处理的错误

写就绪

  • socket内核中,发送缓冲区的可用字节数(发送缓冲区的空闲位置大小)大于等于低水平位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
  • socket的写操作被关闭,对于一个写操作被关闭的sock进行写操作,会触发SIGPIPE信号

select的特点

  • 可监控的文件描述符个数取决于sizeof(fd_set)*8的值
  • 将fd加入select监控集的同时,还要再使用一个数据结构fd_arr保存放到select监控集中的fd。一是用于在select返回后fd_arr作为源数据和fd_set进行FD_ISSET判断。二是select返回之后会把以前加入的但并无事发生的fd清空,则每次开始select前都要重新从fd_arr取得fd逐一加入,扫描fd_arr的同时获取fd最大值max_fd,用于select的第一个参数
  • fd_set的大小可以调整
  • select可以同时等待多个fd,只负责等待,任何一个fd就绪的概率增加,服务器可以在短时间内“等”的比重降低,提高效率

多路转接适用场景:有大量连接,但只有少量活跃->聊天工具

select缺点

在这里插入图片描述

select使用示例

select_server.hpp

#pragma once 
#define BackLog 5
#define NUM 1024
#define DFL_FD -1
#include "sock.hpp"
#include <sys/select.h>

namespace ns_select
{
  class SelectServer
  {
    private:
      int listen_sock;
      unsigned short port;
  
    public:
      SelectServer(unsigned short _port):port(_port)
      {}
      void InitSelectServer()
      {

        listen_sock = ns_sock::Sock::Socket();
        ns_sock::Sock::Bind(listen_sock,port);
        ns_sock::Sock::Listen(listen_sock,BackLog);
      }
      void Run()
      {
        fd_set rfds;
        int fd_arr[NUM] = {0};//临时数组,保存对应的所有文件描述符
        ClearArray(fd_arr,NUM,DFL_FD);//初始化数组中的所有fd设置为无效
        fd_arr[0] = listen_sock;//把监听套接字sock写入数组的第一个元素

        for(;;)
        {
          //时间也是输入输出,如果是间隔性的timeout返回,那么需要对时间进行重新设定
        
          struct timeval timeout = {5,0};//每隔5s timeout一次
          int max_fd = DFL_FD;//不断获取新连接的同时文件描述符在不断增多,所以max_fd值是需要更新
          FD_ZERO(&rfds);//清空所有的read_fd
          //第一次循环的时候fd_arr数组中至少有一个文件描述符:listen_sock;
          for(auto i = 0;i < NUM;i++)
          {
            
            if(fd_arr[i] == DFL_FD)
            {
              continue;
            }
            //需要添加合法fd
            FD_SET(fd_arr[i],&rfds);
            if(max_fd < fd_arr[i])
            {
              max_fd = fd_arr[i];//更新最大描述符
            }
          }
          //1、select阻塞等待 :timeout:nullptr 
          //2、timeout={0} 非阻塞轮询
          //3、阻塞+轮询timeval={5,0}5s之内阻塞等待,5s之后select返回,无论是否有事件就绪
          switch(select(max_fd+1,&rfds,nullptr,nullptr,&timeout))
          {
            case 0:std::cout<<"timeout"<<timeout.tv_sec<<std::endl;
                   break;
            case -1:std::cerr<<"select error"<<std::endl;
                    break;
            default:
                    //正常的事件处理
                    //std::cout<<"有事件发生 "<<"timeout "<<timeout.tv_sec<<std::endl;
                    HandlerEvent(rfds,fd_arr,NUM);//处理事件
                    break;
          }
        }
      }

      void HandlerEvent(const fd_set &rfds,int fd_arr[],int num)
      {
        //怎样判定哪些文件描述符是否就绪?判定特定的fd是否在rfds中
        //都有哪些文件描述符? fd_arr 
        for(auto i = 0;i < num;i++)
        {
          if(fd_arr[i] == DFL_FD)
          {
            continue;
          }
          //是一个合法fd,但不一定就绪
          if(FD_ISSET(fd_arr[i],&rfds) && listen_sock == fd_arr[i])
          {
            //合法fd且已经就绪,连接事件到来
            //accept
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(fd_arr[i],(struct sockaddr*)&peer,&len);//不会阻塞,FD_ISSET已经判定有就绪事件
            if(sock < 0)
            {
              std::cerr<<"accept error"<<std::endl;
              continue;
            }
            //已连接,但是不可以recv,因为连接建立好不代表对端把数据发过来
            //recv是IO:等+拷贝,不知道数据发过来,但是select知道数据有没有就绪
            //不能直接recv,而是将文件描述符添加到fd_arr中因为handlerEvent处理完后会返回到RUN()的循环处
            //下一次循环时,会把所有合法文件描述符重新添加到rfds中-》下一次select就可以检测rfds中读事件是否就绪
            //如果就绪-》HandlerEvent遍历扫描进入“处理正常fd处“进行扫描读取
           // uint16_t  peer_port = htons(peer.sin_port);
            //std::string peer_ip = inet_ntoa(peer.sin_addr);
            //std::cout<<"get a new link! "<<peer_ip<<": "<<peer_port<<std::endl;
            if(!AddToArray(fd_arr,NUM,sock))
            {
              close(sock);
              std::cout<<"select_server is full,close fd: "<<sock<<std::endl;

            }  
          }
          else 
          {
            //处理正常fd
            if(FD_ISSET(fd_arr[i],&rfds))
            {
              //合法fd且已经就绪,读事件就绪
              //实现读写不会阻塞
              char buffer[1024];
              ssize_t s = recv(fd_arr[i],buffer,sizeof(buffer)-1,0);
              if(s > 0)
              {
                buffer[s] = 0;
                std::cout<<"echo# "<<buffer<<std::endl;
              }
              else if(s == 0)
              {
                std::cout<<"client quit"<<std::endl;
                close(fd_arr[i]);
                fd_arr[i] = DFL_FD;//清除数组中的文件描述符
              }
              else 
              {
                std::cout<<"recv error"<<std::endl;
              }
            }
            else 
            {
              //...
            }
          }
        }
        
      }
      ~SelectServer()
      {}
    private:
      void ClearArray(int fd_arr[],int num,int def_fd)
      {
          for(auto i = 0;i < num;i++)
          {
            fd_arr[i] = def_fd;

          }
      }
      bool AddToArray(int fd_arr[],int num,int sock)
      {
        for(auto i = 0;i < num;i++)
        {
          if(fd_arr[i] == DFL_FD)
          {
            //说明该位置没有被使用
            fd_arr[i] = sock;
            return true;
          }
        }
        //数组内空间被使用完毕
        return false;
      }

  };
}

sock.hpp

#pragma once 
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>


namespace ns_sock
{
  class Sock
  {
    public:
      static int Socket()
      {
        int sock = socket(AF_INET,SOCK_STREAM,0);
        if(sock < 0)
        {
          std::cerr<<"sock error"<<std::endl;
          exit(1);
        }
       // int opt = 1;
        //setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
        return sock;
      }
      static bool Bind(int sock,unsigned short port)
      {
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
        {
          std::cout<<"bind error"<<std::endl;
          exit(2);
        }
        return true;

      }
      static bool  Listen(int sock,int backlog)
      {
        if(listen(sock,backlog) < 0)
        {
          std::cerr<<"listen error"<<std::endl;
          exit(3);
        }
        return true;
      }

  };
}

server.cc

#include "select_server.hpp"
#include <iostream>
#include <string>
#include <cstdlib>
static void Usage(std::string proc)
{
  std::cerr<<"Usage:"<<"\n\t"<<proc<<" port "<<std::endl;
}
// ./select_server port

int main(int argc,char*argv[])
{
  if(argc != 2)
  {
    Usage(argv[0]);
    exit(4);
  }
  unsigned short port = atoi(argv[1]);
  ns_select::SelectServer *select_svr = new ns_select::SelectServer(port);
  select_svr->InitSelectServer();
  select_svr->Run();
  return 0;

}

在这里插入图片描述
改进 select_server.hpp

#pragma once 
#define BackLog 5
#define NUM 1024
#define DFL_FD -1
#include "sock.hpp"
#include <sys/select.h>
#include <unordered_map>
namespace ns_select
{
    struct bucket
    {
      public:
        std::string inbuffer;
        std::string outbuffer;
        int in_curr;//已经接收到了多少数据
        int out_curr;//已经发送了多少数据
      public:
        bucket():in_curr(0),out_curr(0)
        {}
    };
  class SelectServer
  {
    private:
      int listen_sock;
      unsigned short port;
      std::unordered_map<int,bucket> buckets;//每来一个新连接,构建一个buckets
    public:
      SelectServer(unsigned short _port):port(_port)
      {}
      void InitSelectServer()
      {

        listen_sock = ns_sock::Sock::Socket();
        ns_sock::Sock::Bind(listen_sock,port);
        ns_sock::Sock::Listen(listen_sock,BackLog);
      }
      void Run()
      {
        fd_set rfds;
        int fd_arr[NUM] = {0};//临时数组,保存对应的所有文件描述符
        ClearArray(fd_arr,NUM,DFL_FD);//初始化数组中的所有fd设置为无效
        fd_arr[0] = listen_sock;//把监听套接字sock写入数组的第一个元素

        for(;;)
        {
          //时间也是输入输出,如果是间隔性的timeout返回,那么需要对时间进行重新设定
        
          struct timeval timeout = {5,0};//每隔5s timeout一次
          int max_fd = DFL_FD;//不断获取新连接的同时文件描述符在不断增多,所以max_fd值是需要更新
          FD_ZERO(&rfds);//清空所有的read_fd
          //第一次循环的时候fd_arr数组中至少有一个文件描述符:listen_sock;
          for(auto i = 0;i < NUM;i++)
          {
            
            if(fd_arr[i] == DFL_FD)
            {
              continue;
            }
            //需要添加合法fd
            FD_SET(fd_arr[i],&rfds);
            if(max_fd < fd_arr[i])
            {
              max_fd = fd_arr[i];//更新最大描述符
            }
          }
          //1、select阻塞等待 :timeout:nullptr 
          //2、timeout={0} 非阻塞轮询
          //3、阻塞+轮询timeval={5,0}5s之内阻塞等待,5s之后select返回,无论是否有事件就绪
          switch(select(max_fd+1,&rfds,nullptr,nullptr,&timeout))
          {
            case 0:std::cout<<"timeout"<<timeout.tv_sec<<std::endl;
                   break;
            case -1:std::cerr<<"select error"<<std::endl;
                    break;
            default:
                    //正常的事件处理
                    //std::cout<<"有事件发生 "<<"timeout "<<timeout.tv_sec<<std::endl;
                    HandlerEvent(rfds,fd_arr,NUM);//处理事件
                    break;
          }
        }
      }

      void HandlerEvent(const fd_set &rfds,int fd_arr[],int num)
      {
        //怎样判定哪些文件描述符是否就绪?判定特定的fd是否在rfds中
        //都有哪些文件描述符? fd_arr 
        for(auto i = 0;i < num;i++)
        {
          if(fd_arr[i] == DFL_FD)
          {
            continue;
          }
          //是一个合法fd,但不一定就绪
          if(FD_ISSET(fd_arr[i],&rfds) && listen_sock == fd_arr[i])
          {
            //合法fd且已经就绪,连接事件到来
            //accept
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(fd_arr[i],(struct sockaddr*)&peer,&len);//不会阻塞,FD_ISSET已经判定有就绪事件
            if(sock < 0)
            {
              std::cerr<<"accept error"<<std::endl;
              continue;
            }
            //已连接,但是不可以recv,因为连接建立好不代表对端把数据发过来
            //recv是IO:等+拷贝,不知道数据发过来,但是select知道数据有没有就绪
            //不能直接recv,而是将文件描述符添加到fd_arr中因为handlerEvent处理完后会返回到RUN()的循环处
            //下一次循环时,会把所有合法文件描述符重新添加到rfds中-》下一次select就可以检测rfds中读事件是否就绪
            //如果就绪-》HandlerEvent遍历扫描进入“处理正常fd处“进行扫描读取
            uint16_t  peer_port = htons(peer.sin_port);
            std::string peer_ip = inet_ntoa(peer.sin_addr);
            struct bucket b;
            buckets.insert({sock,b});
            std::cout<<"get a new link! "<<peer_ip<<": "<<peer_port<<std::endl;
            if(!AddToArray(fd_arr,NUM,sock))
            {
              close(sock);
              std::cout<<"select_server is full,close fd: "<<sock<<std::endl;

            }
            
            
          }
          else 
          {
            //处理正常fd
            if(FD_ISSET(fd_arr[i],&rfds))
            {
              //合法fd且已经就绪,读事件就绪
              //实现读写不会阻塞
              char buffer[1024];
              //不能确定数据都读完了,可能存在粘包/数据丢失问题
              //怎么保证拿到完整的数据?
              //1、定制协议
              //2、给每一个sock定义缓冲区
              buckets[fd.arr[i]].inbuffer = buffer;
              ssize_t s = recv(fd_arr[i],buffer,sizeof(buffer)-1,0);
              if(s > 0)
              {
                buffer[s] = 0;
                std::cout<<"echo# "<<buffer<<std::endl;
              }
              else if(s == 0)
              {
                std::cout<<"client quit"<<std::endl;
                close(fd_arr[i]);
                fd_arr[i] = DFL_FD;//清除数组中的文件描述符
              }
              else 
              {
                std::cout<<"recv error"<<std::endl;
              }

              
            }
            else 
            {
              //...
            }
          }
        }
        
      }
      ~SelectServer()
      {}
    private:
      void ClearArray(int fd_arr[],int num,int def_fd)
      {
          for(auto i = 0;i < num;i++)
          {
            fd_arr[i] = def_fd;

          }
      }
      bool AddToArray(int fd_arr[],int num,int sock)
      {
        for(auto i = 0;i < num;i++)
        {
          if(fd_arr[i] == DFL_FD)
          {
            //说明该位置没有被使用
            fd_arr[i] = sock;
            return true;
          }
        }
        //数组内空间被使用完毕
        return false;
      }

  };
}

poll

在这里插入图片描述

poll函数接口

#include <poll.h>
 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 */
};
  • fds是一个poll函数监听的结构列表,每一个元素中,包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合
  • nfds表示数组的长度
  • timeout表示poll函数的超时时间,单位是毫秒
  • events:用户告知内核,需要关心哪些事件
  • revents:内核告诉用户,哪些文件描述符上的哪些事件就绪了

events和revents的取值
一段二进制序列
在这里插入图片描述

poll优点

不同于select使用三个位图来表示三个fd_set的方式,poll使用一个pollfd指针实现

  • pollfd结构包含了要监视的event和发生的event,不再使用select”参数-值“传递的方式,接口比select方便
  • poll没有最大数量限制(数量过大性能下降)

poll缺点

poll监听的文件描述符增多时

  • 和select函数一样,Poll返回后,需要轮询pollfd来获取就绪的描述符
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中
  • 同时连接的大量客户端在一时刻可能只有很少处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性降低

poll示例

poll_server

#include "sock.hpp"
#include  <poll.h>
#define BackLog 5
namespace ns_poll
{
  class PollServer
  {
    private:
      int listen_sock;
      int port;
    public:
      PollServer(int _port):port(_port)
      {}
      void InitServer()
      {
         listen_sock = ns_sock::Sock::Socket();
        ns_sock::Sock::Bind(listen_sock,port);
        ns_sock::Sock::Listen(listen_sock,BackLog);

      }
      void Run()
      {

        struct pollfd rfds[64];
        for(int i = 0;i < 64;i++)
        {
          rfds[0].fd = listen_sock;
          rfds[0].events |= POLLIN;
          rfds[0].revents = 0;

        }
          rfds[0].fd = listen_sock;
          rfds[0].events |= POLLIN;
          rfds[0].revents = 0;
        for(;;)
        {
          switch(poll(rfds,64,-1))
          {
            case 0:std::cout<<"timeout"<<std::endl;
                   break;
            case 1:std::cout<<"poll error"<<std::endl;
                   break;
            default:
                   for(int i = 0;i < 64;i++)
                   {
                     if(rfds[i].fd == -1)
                     {
                       continue;
                     }
                     if(rfds[i].revents & POLLIN)
                     {
                       if(rfds[i].fd == listen_sock)
                       {
                         //accept
                         std::cout<<"get a new link"<<std::endl;
                       }
                       else 
                       {
                         //recv()
                       }
                     }
                   }
                   break;
          }
        }
      }
      ~PollServer()
      {}
  };
}

sock.hpp

#pragma once 
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>


namespace ns_sock
{
  class Sock
  {
    public:
      static int Socket()
      {
        int sock = socket(AF_INET,SOCK_STREAM,0);
        if(sock < 0)
        {
          std::cerr<<"sock error"<<std::endl;
          exit(1);
        }
        int opt = 1;
        setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
        return sock;
      }
      static bool Bind(int sock,unsigned short port)
      {
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
        {
          std::cout<<"bind error"<<std::endl;
          exit(2);
        }
        return true;

      }
      static bool  Listen(int sock,int backlog)
      {
        if(listen(sock,backlog) < 0)
        {
          std::cerr<<"listen error"<<std::endl;
          exit(3);
        }
        return true;
      }

  };
}

server.cc

#include "poll_server.hpp"
#include <iostream>
#include <string>
#include <cstdlib>
static void Usage(std::string proc)
{
  std::cerr<<"Usage:"<<"\n\t"<<proc<<" port "<<std::endl;
}
// ./select_server port

int main(int argc,char*argv[])
{
  if(argc != 2)
  {
    Usage(argv[0]);
    exit(4);
  }
  unsigned short port = atoi(argv[1]);
  ns_poll::PollServer *ps= new ns_poll::PollServer(port);
  ps->InitServer();
  ps->Run();
  return 0;

}

在这里插入图片描述

epoll

epoll是为了处理大批量句柄而作了改进的poll
公认为Linux2.6下性能最好的多路I/O就绪通知方法

epoll相关系统调用

在这里插入图片描述

epoll_create

 #include <sys/epoll.h>
  int epoll_create(int size);

创建一个epoll句柄

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

epoll_ctl

 #include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll的事件注册函数

  • 在这里先注册要监听的事件类型
  • epfd:是epoll_create()的返回值(epoll的句柄)
  • op:表示动作,用三个宏来表示
  • fd :需要监听的fd
  • event:告诉内核需要监听什么事

第二个参数取值

  • EPOLL_CTL_ADD:注册新的fd到epfd中,往红黑树中添加节点
  • EPOLL_CTL_MOD:修改已经注册的fd监听事件,修改红黑树中特点节点的数据
  • EPOLL_CTL_DEL:从epfd中删除一个fd,删除红黑树的特点的节点

struct epoll_event 结构:

struct epoll_event {
 uint32_t     events;      /* Epoll events */
 epoll_data_t data;        /* User data variable */
 };
 typedef union epoll_data {
 	void        *ptr;
	int          fd;
	uint32_t     u32;
	uint64_t     u64;
} epoll_data_t;

events可以是以下几个宏的集合

  • EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
  • EPOLLOUT:表示对应的文件描述符可以写
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(外数据到来)
  • EPOLLERR:表示对应的文件描述符发生错误
  • EPOLLHUP:表示对应的文件描述符被挂断
  • EPOLLET:将EPOLL设置为边缘触发模式,这是相对水平触发来说的
  • EPOLLONESHOT:只监听一次事件,当监听完此次事件后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

epoll_wait

 #include <sys/epoll.h>
 int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);

收集在epoll监听的事件中已经发送的事件

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

epoll工作原理

在这里插入图片描述

  • 当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关
struct eventpoll
{
	struct rb_root rbr;
	//红黑树的根节点,这棵树中存储着所有添加到epoll的需要监控的事件
	struct list_head rdlist;
	//双链表中则存放着将要通过epoll_wait返回给客户的满足条件的事件
}
  • 每一个epoll对象都有一个独立的eventpoll的结构体,用于存放通过epoll_ctl方法向epoll对象添加进来的事件
  • 这些事件都会挂载在红黑树中,如此反复添加的事件就可以通过红黑树而高效的识别处理(红黑树插入时间效率lgN,N为树高度)
  • 所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,当响应事件发生时会调用这个回调方法
  • 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中
  • 在epoll中,对于每一个事件,都会建立一个epitem的结构体
struct epitem
{
	struct rb_node rbn;//红黑树节点
	struct list_head;//双向链表指针
	struct epoll_filefd ffd;//事件句柄信息
	struct eventpoll *ep;//指向其所属事件的eventpoll对象
	struct epoll_event event;//期待发生的事件类型
}
  • 当调用epoll_wait检测是否有事件发生时,只需要检测eventpoll对象中的rdlist双链表中是否有epitem元素
  • 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户,这个操作时间复杂度为O(1)

总结epoll的使用过程

  • 调用epoll_create创建一个epoll句柄
  • 调用epoll_ctl将要监控的文件描述符进行注册
  • 调用epoll_wait,等待文件描述符就绪

epoll的优点

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

我们定义的struct epoll_event是我们在用户空间中分配好的内存,还是需要将内核的数据拷贝到这个用户空间的内存中的

读取数据的epoll服务器

epoll_sever.hpp

#include "sock.hpp"
#pragma once
#include <sys/epoll.h>
#define max_num 64
#define backlog 5
namespace  ns_epoll
{
  const int size = 256;
  class EpollServer 
  {
    private:
      int listen_sock;
      int epfd;
      uint16_t port;
    public:
      EpollServer(int _port):port(_port)
      {}
    public:
      void InitEpollServer()
      {
        listen_sock = ns_sock::Sock::Socket();
        ns_sock::Sock::Bind(listen_sock,port);
        ns_sock::Sock::Listen(listen_sock,backlog);
        std::cout<<"debug,listen_sock "<<listen_sock<<std::endl;//3
        if((epfd=epoll_create(size)) < 0)
        {
          std::cerr<<"epoll error"<<std::endl;
          exit(4);
        }
        std::cout<<"debug epfd"<<epfd<<std::endl;//4
      }
      void AddEvent(int sock,uint32_t event)
      {
        struct epoll_event ev;
        ev.events = 0;
        ev.events |= event;//非必须
        ev.data.fd = sock;//哪个文件描述符就绪
        if(epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev) < 0)
        {
          std::cerr<<"epoll_ctl error "<< sock <<std::endl;
          
        }
      }
      void DelEvent(int sock)
      {
        if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr) < 0)
        {
          std::cerr<<"EPOLL_CTL_DEL error fd:"<< sock <<std::endl;
        }
      }
      void Run()
      {
        //这里目前只有一个socket是能够关心读写的:listen_sock,read event
        AddEvent(listen_sock,EPOLLIN);
        //int timeout = 1000;
        int timeout = -1;//默认情况下,epoll有事件就绪但是没有处理,会一直通知你
      
        struct epoll_event revs[max_num];
        for(;;)
        {
          //返回值num表明有多少个事件就绪,内核会将就绪事件依次放入revs中
          int num = epoll_wait(epfd,revs,max_num,timeout);
          if(num > 0)
          {
            for(int i = 0;i < num;i++)
            {
              int sock = revs[i].data.fd;//哪个文件描述符就绪?
              if(revs[i].events & EPOLLIN)
              {
                //读事件就绪
                //1、listen_sock 连接事件就绪
                if(sock == listen_sock)
                {
                  struct sockaddr_in peer;
                  socklen_t len = sizeof(peer);
                  int sk = accept(listen_sock,(struct sockaddr*)&peer,&len);
                  if(sk < 0)
                  {
                    std::cout<<"accept error"<<std::endl;
                    continue;
                  }
                  //不能直接recv
                  std::cout<<"get a new link" << inet_ntoa(peer.sin_addr)<<":"<<ntohs(peer.sin_port)<<std::endl;
                  AddEvent(sk,EPOLLIN);//先进行读取,只有需要写的时候才会主动设置EPOLLOUT

  
                }
                else 
                {
                   //2、sock可读事件就绪
                   char buffer[1024];
                   ssize_t s = recv(sock,buffer,sizeof(buffer) - 1,0);
                   if(s > 0)
                   {
                     buffer[s] = 0;
                     std::cout<<buffer<<std::endl;
                   }
                   else
                   {
                     std::cout<<"client quit"<<std::endl;
                     close(sock);
                     DelEvent(sock);
                   }
                }
              }
              else if(revs[i].events & EPOLLOUT)
              {

              }
              else 
              {

              }
            }
            //std::cout<<"有事件发生了"<<std::endl;
          }
          else if(num == 0)
          {
            std::cout<<"timeout"<<std::endl;
          }
          else 
          {
            std::cerr<<"epoll error"<<std::endl;
          }
        }
      }
      ~EpollServer()
      {
        if(listen_sock >= 0) close(listen_sock);
        if(epfd >= 0) close(epfd);
      }
  };
}

sock.hpp

#pragma once 
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>


namespace ns_sock
{
  class Sock
  {
    public:
      static int Socket()
      {
        int sock = socket(AF_INET,SOCK_STREAM,0);
        if(sock < 0)
        {
          std::cerr<<"sock error"<<std::endl;
          exit(1);
        }
        int opt = 1;
        setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
        return sock;
      }
      static bool Bind(int sock,unsigned short port)
      {
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
        {
          std::cout<<"bind error"<<std::endl;
          exit(2);
        }
        return true;

      }
      static bool  Listen(int sock,int backlog)
      {
        if(listen(sock,backlog) < 0)
        {
          std::cerr<<"listen error"<<std::endl;
          exit(3);
        }
        return true;
      }

  };
}

server.cc

#include "epoll_server.hpp"
#include <string>
#include <cstdlib>
static void Usage(std::string proc)
{
  std::cerr<<"Usage:"<<"\n\t"<<proc<<" port "<<std::endl;
}
// ./epoll_server port

int main(int argc,char*argv[])
{
  if(argc != 2)
  {
    Usage(argv[0]);
    exit(4);
  }
  int  short port = atoi(argv[1]);
  ns_epoll::EpollServer *es= new ns_epoll::EpollServer(port);
  es->InitEpollServer();
  es->Run();
  return 0;

}

在这里插入图片描述

epoll的工作方式

epoll有两种工作方式:水平触发(LT)和边缘触发(ET)
例子:

  • 把一个TCP socket添加到epoll描述符此时socket的另一端被写入了2kb的数据
  • 调用epoll_wait,并且它会返回,说明已经准备好读取数据
  • 调用read,只读取1kb的数据
  • 继续调用epoll_wait…

水平触发(LT)工作模式:数据没被取走一直通知上层

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

边缘触发(ET)工作模式:数据从无->有,从有->多(变化)会通知上层

  • 当epoll_wait检测到socket事件就绪时,必须立刻处理
  • 如上面的例子,虽然只读了1kb的数据,缓冲区还剩1kb的数据,在第二次调用,epoll_wait的时候,epoll_wait不再返回
  • ET模式下,文件描述符的事件就绪后只有1次的处理机会
  • ET的性能比LT性能更高(epoll_wait返回次数少了很多),默认采用ET模式使用epoll_wait
  • 只支持非阻塞的读写

select/poll是在工作在LT模式下,epoll既支持LT也支持ET
在这里插入图片描述

对比LT和ET

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

ET模式和非阻塞文件描述符

使用ET模式的epoll,需要将文件描述符设置为非阻塞
在这里插入图片描述
但是问题来了:

  • 服务器只读到1kb数据,需要10kb数据读完才会给客户端返回响应数据
  • 客户端要读到服务器的响应才会发送下一个请求
  • 客户端发送了下一个请求,epoll_wait才会返回,才能去读取缓冲区中剩余的数据

所以,为了解决上述问题(阻塞read不一定能把完整的请求读完),于是就可以使用非阻塞轮询的方式来读取缓冲区,保证一定能把完整的请求都读出来
而如果是LT,只要缓冲区的数据没有读完,就够让epoll_wait返回文件描述符读就绪

epoll的使用场景

epoll的高性能,是有一定的特定场景的,如果场景选的不适宜,epoll的性能可能适得其反

  • 对于多链接,且连接中只能由一部分比较活跃时,适合使用epoll

eg:需要处理上万个客户端的服务器;各种互联网APP的入口服务器
如果是系统内部,服务器和服务器之间进行通信,只有少数几个连接,这种情况下使用epoll就不合适

基于Reactor的网络版计算器

在这里插入图片描述

Reactor.hpp

#include "sock.hpp"
#pragma once
#include <sys/epoll.h>
#include <string>
#include <unordered_map>
#define max_num 64
#define backlog 5
namespace  ns_epoll
{
  class Reactor;
  class EventItem;
  const int size = 256;
  typedef int(*callback_t)(EventItem *);//const &:输入;*:输出;&:输入输出
  class EventItem//事件的元素
  {
    public:
      int sock;//通信相关
      Reactor *R;//回指Reactor
      //有关数据处理的回调函数,用来进行逻辑解耦的
      //应用层数据就绪等通信细节和数据的处理模块使用该方法进行解耦
      callback_t recv_handler;
      callback_t send_handler;
      callback_t error_handler;

      std::string inbuffer;//读取到该数据的缓冲区
      std::string outbuffer;//发生的数据缓冲区

    public:
      EventItem():sock(0),R(nullptr),recv_handler(nullptr),send_handler(nullptr),error_handler(nullptr)
      {}
      ~EventItem()
      {}
      void ManagerCallBack(callback_t _recv,callback_t _send,callback_t _error)
      {
        recv_handler = _recv;
        send_handler = _send;
        error_handler = _error;
      }
  };
  class Reactor 
  {
    private:
      int epfd;
      std::unordered_map<int,EventItem> event_items;//将sock映射到EventItem,可以拿着sock直接找到对应的Event_item

    public:
      Reactor():epfd(-1)
      {}
    public:
      void InitReactor()
      {
        if((epfd=epoll_create(size)) < 0)
        {
          std::cerr<<"epoll error"<<std::endl;
          exit(4);
        }
        std::cout<<"debug epfd"<<epfd<<std::endl;//4
      }
      void AddEvent(int sock,uint32_t event,const EventItem &item)
      { 
        struct epoll_event ev;
        ev.events = 0;
        ev.events |= event;//非必须
        ev.data.fd = sock;//哪个文件描述符就绪
        if(epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev) < 0)
        {
          std::cerr<<"epoll_ctl error "<< sock <<std::endl;
          
        }
        else 
        {
          //EventItem item;
          event_items.insert({sock,item});
        }
        std::cout<<"debug:"<<"添加"<<sock<<"到epoller中,成功"<<std::endl;
      }
      void DelEvent(int sock)
      {
        if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr) < 0)
        {
          std::cerr<<"EPOLL_CTL_DEL error fd:"<< sock <<std::endl;
        }
        event_items.erase(sock);
      }

      void EnableReadWrite(int sock,bool read,bool write)
      {
        //之前没有设置EPOLLOUT
        struct epoll_event evt;
        evt.data.fd = sock;
        evt.events = (read ? EPOLLIN: 0)|(write ? EPOLLOUT:0) | EPOLLET;
        if(epoll_ctl(epfd,EPOLL_CTL_MOD,sock,&evt) < 0)
        {
          std::cerr<<"epoll_ctl_mod error,fd"<<sock<<std::endl;
        }
      }


      void Dispatcher(int timeout)//事件分派器
      {
        //如果底层的事件就绪,就把对应事件分派给指定的回调函数进行同一处理
        struct epoll_event revs[max_num];
        for(;;)
        {
          //返回值num表明有多少个事件就绪,内核会将就绪事件依次放入revs中
          int num = epoll_wait(epfd,revs,max_num,timeout);
          for(int i = 0;i < num;i++)
          {
            int sock = revs[i].data.fd;
            uint32_t mask = revs[i].events;
            if(revs[i].events &EPOLLIN)
              //读事件就绪
              if(event_items[sock].recv_handler)//读事件就绪
                event_items[sock].recv_handler(&event_items[sock]);//如果读取回调函数被设置,调用回调函数
            if(revs[i].events &EPOLLOUT)
              if(event_items[sock].send_handler)
                event_items[sock].send_handler(&event_items[sock]);
            if((revs[i].events &EPOLLERR)||(revs[i].events & EPOLLHUP))
            {
              //把所有的异常事件,交给read,write处理
              if(event_items[sock].error_handler)
                //event_items[sock].error_handler(&event_items[sock]);
                mask |= (EPOLLIN|EPOLLOUT);
            }

          }
        }
      }
       ~Reactor()
      {
        //if(listen_sock >= 0) close(listen_sock);
        if(epfd >= 0) close(epfd);
      }
  };
}

app_interface.hpp

#pragma once
#include <iostream>
#include <vector>
#include <string>
#include "util.hpp"
#include "Reactor.hpp"
namespace  ns_app
{
  using namespace ns_epoll;
  int recver(EventItem* item);
  int sender(EventItem* item);
  int errorer(EventItem* item);
  int accepter(EventItem* item)
  {
    //std::cout<<"监听套接字来了新连接,执行回调函数"<<std::endl;
    std::cout<<"get a new link"<<item->sock << std::endl; 
    while(true)
    {
      struct sockaddr_in peer;
      socklen_t len = sizeof(peer);
      int sock = accept(item->sock,(struct sockaddr*)&peer,&len);
      if(sock < 0)
      {
        if(errno == EAGAIN || errno == EWOULDBLOCK)
        {
          //说明没有读取出错,只是底层没有链接
          return 0;
        }
        if(errno == EINTR)
        {
          //读取的过程被信号打断
          continue;
        }
        else 
        {
          //真正出错
          return -1;
        }
      }
      else 
      {
        //读取成功
        ns_util::SetNoBlock(sock);
        EventItem tmp;
        tmp.sock = sock;
        tmp.R = item->R;
        tmp.ManagerCallBack(recver,sender,errorer);
        Reactor *epoller = item->R;
        //epoll经常会设置读事件就绪,而写事件会按需打开
        epoller->AddEvent(sock,EPOLLIN|EPOLLET,tmp);//将新获取的链接托管给对应的epoller
      }
    }
    return 0;
  }
  int recv_helper(int sock,std::string *out)
  {
    //0:读取成功;-1:读取失败
    while(true)
    {
      char buffer[128];
      ssize_t size = recv(sock,buffer,sizeof(buffer)-1,0);
      if(size < 0)
      {
        if(errno == EAGAIN || errno == EWOULDBLOCK)
        {
          //循环读取
          return 0;
        }
        else if(errno == EINTR)
        {
          //被信号中断,继续尝试
          continue;
        }
        else 
        {
          //真正出错
          return -1;
        }
      }
      else 
      {
        buffer[size] = 0;
        *out += buffer;
      }
    }
  }
  int recver(EventItem* item)
  {
    std::cout<<"recv event ready:"<<item->sock<<std::endl;
    //负责数据读取
    //1、需要整体读,非阻塞
    if(recv_helper(item->sock,&(item->inbuffer)) < 0)
    {
      //读取失败
      return -1;
    }
    std::cout <<"client# "<<item->inbuffer<<std::endl;
   
    //2、根据发来的数据流,进行包和包之间的分离,防止粘包问题,涉及到协议定制
    
    std::vector<std::string> message;
    ns_util::StringUtil::Split(item->inbuffer,&message,"X");
    //3、针对一个一个的报文协议进行反序列化decode
    /*
    for(auto s : message)
    {
      std::cout<<"##################################"<<std::endl;
      std::cout<<"提取出"<<s<<std::endl;
      std::cout<<"##################################"<<std::endl;
    }
      std::cout<<"剩余 "<<item->inbuffer<<std::endl;
*/
    struct data 
    {
      int x;
      int y;
    };
    //已经拿到了所有报文数据
    for(auto s: message)
    {
      struct data d;
      ns_util::StringUtil::Deserialize(s,&d.x,&d.y); 
      std::cout<<d.x<<":"<<d.y<<std::endl;

      //Reactor如果只负责读取数据流,进行报文和报文分离,不再进行后续处理,
      //形成报文,构建响应等工作,交给软件层或线程进程池处理,基于Reactor的
      //半同步半异步的工作方式,是Linux中最常用的工作方式
      //4、业务处理
      int z = d.x+d.y;
      //5、形成响应报文,序列化转化成为一个字符串encode
      std::string response;
      response += std::to_string(d.x);
      response += "+";
      response += std::to_string(d.y);
      response += " = ";
      response += std::to_string(z);

      item->outbuffer += response;
      //5-1设置响应报文和响应报文之间的分隔符
      item->outbuffer += "X";//encode

    }
    //6、写回
    if(!item->outbuffer.empty())
      item->R->EnableReadWrite(item->sock,true,true);
   
    return 0;
  }
  int send_help(int sock,std::string& in)
  {
    //0:缓冲区打满,并且写完 1:缓冲区打满,下次写入-1:error,写出错 
    while(true)
    {
        size_t total = 0;
        ssize_t s = send(sock,in.c_str()+total,in.size()-total,0);
        if(s > 0)
        {
          total += s;
          if(total >= in.size())
          {
             return 0;

          }
        }
        else if(s < 0)
        {
          if(errno == EAGAIN ||errno == EWOULDBLOCK)
          {
            //无论是否发送完,inbuffer,都需要将已经发送的数据全部移出缓冲区
            in.erase(total);
            return 1;//已经将缓冲区打满,不能再写入
          }
          else if(errno == EINTR)
          {
            continue;
          }
          else 
          {
            return -1;
          }
        }
    }
  }
  int sender(EventItem* item)
  {
    int ret = send_help(item->sock,item->outbuffer);
    if(ret  == 0)
    {
      item->R->EnableReadWrite(item->sock,true,false);

    }
    else if(ret == 1)
    {
      //只要设置了epollout,默认epoll会在下一次自动触发
      item->R->EnableReadWrite(item->sock,true,true);
    }
    else 
    {

    }
    return 0;
  }
  int errorer(EventItem* item)
  {
    return 0;
  }
}

util.hpp

#pragma once
//工具类
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <vector>
namespace ns_util
{
  void SetNoBlock(int sock)
  {
    int fl = fcntl(sock,F_GETFL);
    fcntl(sock,F_SETFL,fl | O_NONBLOCK);
  }
  class StringUtil
  {
    public:
      static void Split(std::string &in,std::vector<std::string> *out,std::string sep)
      {
        //aaaXbbbXcccX->X代表一个完整报文结束 
        //aaXbXcc
        //aa 
        //;;
        while(true)
        {
          size_t pos = in.find(sep);
          if(pos == std::string::npos)
          {
            break;
          }
          
          sleep(1);
          std::string s = in.substr(0,pos);
        //  std::cout<<s<<std::endl;
          out->push_back(s);
          in.erase(0,pos+sep.size());
        }
      }
      static void Deserialize(std::string &in,int *x,int *y)//反序列化
      {
        size_t pos = in.find("+");
        std::string left = in.substr(0,pos);
        std::string right = in.substr(pos+1);
        *x = atoi(left.c_str());
        *y = atoi(right.c_str());

      }
  };
}

sock.hpp

#pragma once 
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>


namespace ns_sock
{
  class Sock
  {
    public:
      static int Socket()
      {
        int sock = socket(AF_INET,SOCK_STREAM,0);
        if(sock < 0)
        {
          std::cerr<<"sock error"<<std::endl;
          exit(1);
        }
        int opt = 1;
        setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
        return sock;
      }
      static bool Bind(int sock,unsigned short port)
      {
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
        {
          std::cout<<"bind error"<<std::endl;
          exit(2);
        }
        return true;

      }
      static bool  Listen(int sock,int backlog)
      {
        if(listen(sock,backlog) < 0)
        {
          std::cerr<<"listen error"<<std::endl;
          exit(3);
        }
        return true;
      }

  };
}

server.cc

#include "sock.hpp"
#include "util.hpp"
#include "Reactor.hpp"
#include "app_interface.hpp"
#include <string>
#include <cstdlib>
#define back_log 5
static void Usage(std::string proc)
{
  std::cerr<<"Usage:"<<"\n\t"<<proc<<" port "<<std::endl;
}
// ./epoll_server port

int main(int argc,char*argv[])
{
  if(argc != 2)
  {
    Usage(argv[0]);
    exit(4);
  }
  //与listen_sock相关
  uint16_t  port = atoi(argv[1]);
  int listen_sock = ns_sock::Sock::Socket();
  ns_util::SetNoBlock(listen_sock);
  ns_sock::Sock::Bind(listen_sock,port);
  ns_sock::Sock::Listen(listen_sock,back_log);

  std::cout<<"listen_sock"<<listen_sock<<std::endl;
  //与epoll事件管理器相关
  ns_epoll::Reactor R;
  R.InitReactor();
  //需要把listen_sock添加到epoll事件管理器中
  ns_epoll::EventItem item;
  item.sock = listen_sock;
  item.R = &R;
  //listen_sock只需要关系读事件就可以
  item.ManagerCallBack(ns_app::accepter,nullptr,nullptr);
  //将Listen_sock托管给epoller
  R.AddEvent(listen_sock,EPOLLIN | EPOLLET,item);
  int timeout = 1000;
  while(true)
  {
    R.Dispatcher(timeout);
  }
  return 0;

}

在这里插入图片描述
在这里插入图片描述

评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值