Linux_网络 IO多路转接poll与epoll、详解epoll(工作原理,参数解析,工作方式、优缺点对比)、实例poll服务器 epoll服务器、Reactor设计模式ET工作epoll服务器

14 篇文章 3 订阅

IO多路转接之select

1. IO多路转接 poll

poll与select的区别:

  1. poll解决的select检测文件描述符数目有限的缺陷。
  2. poll解决了select需要重复添加文件描述符。poll将用户传递给内核结构体与内核给用户的结构体分开,不用每次调用poll时重新添加文件描述符。

在这里插入图片描述

fds:需要管理的文件描述符。
nfds:需要管理文件描述符的个数。
timeout_ts:设置超时时间。

  • 设置1000:每1秒timeout一次。
  • 设置0:非阻塞轮询。
  • 设置-1:阻塞

返回值:文件描述符就绪的数目。
小于0代表失败。
等于0代表超时。

在这里插入图片描述
poolfd结构体如上图:
event:用户告诉内核需要关心的文件描述符。
revent:内核告诉用户,那些文件描述符就绪。
在这里插入图片描述
关心的事件如上图,这个宏都是一段二进制序列。需要重复添加使用 | 即可,判断是否就绪用&对应的事件不为0就代表这个事件就绪
重点只关注两个POLLIN与POLLOUT这两个宏。

2. 实例:IO多路转接poll

封装套接字:

#pragma once

#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<string.h>

namespace NetWork_Socket{
  class Sock{
    public:
      static int Socket(){
        //创建监听套接字
        int listenSock=socket(AF_INET,SOCK_STREAM,0);
        if(listenSock<0){
          std::cout<<"socket error"<<std::endl;
          exit(-1);
        }
        int opt=1;
        //设置套接字属性
        setsockopt(opt,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
        return listenSock;
      }

      static bool Bind(int listenSock,int port){
        //绑定,IP=INADDR_ANY
        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(listenSock,(struct sockaddr*)&local,sizeof(local))<0){
          std::cout<<"bind error"<<std::endl;
          exit(-2);
        }
        return true;
      }

      static int Listen(int listenSock,int Len){//全连接队列长度
        //监听
        if(listen(listenSock,Len)<0){
          std::cout<<"listen error"<<std::endl;
          exit(-3);
        }
        return true;
      }
  };
}

poll多路转接服务器

#include"sock.h"
#include"poll.h"

namespace IO_POLL{
  class POll_Sever{
    private:
      int listen_sock;
      int port;
    public:
      POll_Sever(int _port):port(_port){
        listen_sock=NetWork_Sorket::Sork::Socket();
      }

      void InitSever(){
        NetWork_Sorket::Sork::Bind(listen_sock,port);
        NetWork_Sorket::Sork::Listen(listen_sock,5);
      }

      void Start(){
        struct pollfd rfds[64];
        for(int i=0;i<64;i++)//初始化结构体
        {
          rfds[i].fd=-1;
          rfds[i].events=0;
          rfds[i].revents=0;//->
        }
        rfds[0].fd=listen_sock;
        rfds[0].events |=POLLIN;
        rfds[0].revents=0;
        while(true){
          switch(poll(rfds,64,-1)){
            case 0:
              std::cout<<"time out"<<std::endl;
              break;
            case -1:
              std::cerr<<"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 link!"<<std::endl;
                    //将accept获取的连接添加到数组中
                  }
                  else{
                    //recv
                    //将文件描述符从数组中删除
                  }
                }
              }
              break;
          }
        }
      }

      ~POll_Sever(){}
  };
}

poll可以监视套接字数量与pollfd数组大小有关,解决了select监视文件描述符数量有限的问题。

poll的优缺点

优点:

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

缺点:

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

3. IO多路转接epoll

epoll与select和poll思路相同,作用都是等待资源就绪。

使用epoll需要三个接口。

  1. epoll_create:创建epoll模型(epoll对应的数据结构)
    在这里插入图片描述

返回值是文件描述符。失败返回-1。

参数size:一般填写128于256。随意填写,现在标准弃用了这个参数,为了兼容之前版本。


  1. epoll_ctl:用户向epoll模型中添加需要关心文件描述符与事件。

在这里插入图片描述

select与epolll来讲,数据流有两个方向。

①用户–>内核 和②内核–>用户。

epoll解决1问题时是用epoll_ctl()函数。
epoll_ctl函数解决了用户向内核添加,删除某些文件描述符以及对应文件描述符所关心的事件

参数解析在epoll工作原理哪里。


  1. epoll_wait:等待文件描述符就绪。
    在这里插入图片描述
    epfd:表示等待那个epoll模型。

所以:进程可以创建多个epoll模型。

events和maxevents参数是解决②内核–>用户问题的,用户通过这两个参数那到内核通知用户那些事件已经就绪。

  • events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助在用户态中分配内存).
  • maxevents告知内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.

timeout参数以毫秒为单位。

函数返回0代表超时,-1代表失败。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目

epoll的工作原理

在这里插入图片描述
综上:epoll_ctl函数本质是修改红黑树。
在这里插入图片描述
epfd:操作的epoll模型
op:表示操作动作,用三个宏表示。

  • EPOLL_CTL_ADD:注册新的fd到epfd中;(向红黑树中新加节点)
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;(修改红黑树某个节点的数据)
  • EPOLL_CTL_DEL:从epfd中删除一个fd;(删除红黑树的特定节点)

event:
在这里插入图片描述

结构体中的events,需要关注文件描述符的那些时间。
events的宏值有:每个宏只占1比特位,可以通过位运算传多个参数
在这里插入图片描述

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

结构体中的date:
将事件就绪的套接字设置到date的fd项中,为例区分监听套接字还是普通套接字,详情见实例。

同时:

  1. epoll不需要用户自己对红黑树进行维护,这些操作都在内核中有系统进行维护。
  2. 文件描述符就绪后调用回调函数,封装成节点插入到就绪队列。epoll_wait函数拿取就绪队列上的节点可以看作一个生产者消费者模型
  3. epoll_creat函数创建epoll模型是指创建红黑树和就绪队列,注册回调函数。
  4. epoll函数底层加锁,是线程安全的。
  5. 所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法。

注意:用户区定义epoll_event结构体数组,内核最后将返回的数据拷贝到数组中,一定要涉及拷贝过程。不存在系统通过内存映射机制将task_struct与内核数据建立映射,避免系统拷贝数据的说法。操作系统不可能让用户直接访问内核的就绪队列。

epoll优点(和select缺点对比)

  1. 接口使用方便,不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开。
  2. 数据拷贝轻量,调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  3. 监测的文件描述符数量无限制。
  4. 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1).(内核将就绪节点拷贝到用户区事件复杂度O(N)不可避免)。

4. 实例:IO多路转接epoll

epoll服务器流程如下:

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

封装套接字

#pragma once

#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<string.h>

namespace NetWork_Sorket{
  class Sock{
    public:
      static int Socket(){
        //创建监听套接字
        int listenSock=socket(AF_INET,SOCK_STREAM,0);
        if(listenSock<0){
          std::cout<<"socket error"<<std::endl;
          exit(-1);
        }
        int opt=1;
        //设置套接字属性
        setsockopt(opt,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
        return listenSock;
      }

      static bool Bind(int listenSock,int port){
        //绑定,IP=INADDR_ANY
        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(listenSock,(struct sockaddr*)&local,sizeof(local))<0){
          std::cout<<"bind error"<<std::endl;
          exit(-2);
        }
        return true;
      }

      static int Listen(int listenSock,int Len){//全连接队列长度
        //监听
        if(listen(listenSock,Len)<0){
          std::cout<<"listen error"<<std::endl;
          exit(-3);
        }
        return true;
      }
  };
}

epoll服务器

#pragma once

#include"sock.h"
#include<sys/epoll.h>

namespace EpollSever{

#define MAXSIZE 64
  const int Back_Log=5;

  class epoll_sever{
    private:
      int listen_sock;
      int epfd;
      int port;
    public:
      epoll_sever(int _port):port(_port){
        listen_sock=NetWork_Sorket::Sock::Socket();
        NetWork_Socket::Sock::Bind(listen_sock,port);
        NetWork_Socket::Sock::Listen(listen_sock,Back_Log);
        epfd=epoll_create(256);
        if(epfd<0){//256无意义->
          std::cerr<<"epoll create error"<<std::endl;
          exit(4);
        }
      }

      void Start(){
        //将listen_sock添加到红黑树中,关心读
        AddEvent(listen_sock,EPOLLIN);
        int timeout=1000;
        
        struct epoll_event fd_events[MAXSIZE];//MAXSIZE不能额大于创建epoll时的size及256
        while(true){
          int num=epoll_wait(epfd,fd_events,MAXSIZE,timeout);
          //内核会将就绪事件依次放入数组中,不会做重复遍历
          if(num>0){
            std::cout<<"epoll wait succeed!"<<std::endl;
            for(int i=0;i<num;i++){
              int sock=fd_events[i].data.fd;
              if(fd_events[i].events&EPOLLIN){
                //读事件就绪
                if(sock==listen_sock){
                  struct sockaddr_in peer;
                  socklen_t len=sizeof(peer);
                  int sock=0;
                  if((sock=accept(listen_sock,(struct sockaddr*)&peer,&len))<0){
                    std::cerr<<"accept error"<<std::endl;
                    continue;
                  }
                  std::cout<<"get a new link"<<std::endl<<"ip: "<<inet_ntoa(peer.sin_addr)<<" port"<<ntohs(peer.sin_port)<<std::endl;
                  AddEvent(sock,EPOLLIN);//先监测读事件就绪情况,只有主动写时才设置EPOLLOUT
                }
                else{
                  char buff[1024];
                  ssize_t size=recv(sock,buff,sizeof(buff)-1,0);//存在数据读不完或粘包问题
                  if(size>0){
                    buff[size]=0;
                    std::cout<<buff<<std::endl;
                  }
                  else{
                    std::cout<<"client close !"<<std::endl;
                    close(sock);
                    DelEvent(sock);
                  }
                }
              }
              else if(fd_events[i].events&EPOLLOUT){
                //写事件就绪
                //......
              }
            }
          }
          else if(num==0){
            std::cout<<"time out"<<std::endl;
          }
          else{
            std::cerr<<"epoll wait error"<<std::endl;
          }
        }
      }

      ~epoll_sever(){
        if(listen_sock>=0){
          close(listen_sock);
        }
        if(epfd>=0){
          close(epfd);
        }
      }
    private:
      void AddEvent(int sock,uint32_t event){
        struct epoll_event ev;
        memset(&ev,0,sizeof(ev));
        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::cout<<"epoll ctl del error"<<std::endl;
        }
      }
  }; 
}
#include"epoll_sever.h"
#include<stdlib.h>

void UsrHelp(const char*name){
  std::cout<<"UsrHelp# "<<name<<" +port"<<std::endl;
}

int main(int argc,char*argv[]){
  if(argc!=2){
    UsrHelp(argv[0]);
    exit(5);
  }
  int port=atoi(argv[1]);

  EpollSever::epoll_sever*sever=new EpollSever::epoll_sever(port);
  sever->Start();
  return 0;
}

在这里插入图片描述

5. epoll工作方式

epoll有两种工作方式,水平触发(LT)与边缘触发(ET)

水平触发(LT):只要底层有数据没有被取走,会一直通知上层,需要上层读取数据。

边缘触发(ET):只有当底层数据从无到有,或者数据变化时,会通知上层一次,需要上层读取数据。

epoll告知上层数据就绪的方式有上述两种方式。


边缘触发Edge Triggered工作模式:

只有当底层数据从无到有,或者数据变化时,会通知上层一次,需要上层一次将数据全部读走。

所以:ET模式会倒逼应用层立即将就绪的数据全部读取完毕。
为了保证recv函数能够将发来的数据全部读取完毕,需要循环调用recv函数,直到读取大小 小于要读取的值时,说明数据已经被读取完毕。最后将读取的数据拼接起来即可。

注意:ET模式下,recv/write函数必须设置成非阻塞读取模式
假设ET模式下recv函数是阻塞读取的话:
如果数据共有300字节,每次recv读取100字节,不满足上面说的读取结束要求,会进行第四次循环读取,第四次循环recv时因为无数据会阻塞等待,这个线程会被挂起。无法再响应任何外部事件。

  • ET模式仅支持非阻塞recv/write函数。

水平触发Level Triggered 工作模式:

只要底层有数据没有被取走,会一直通知上层,需要上层读取数据。

LT模式不要求一次将数据读取完毕,但是如果设置成要一次读取完毕,会面临和ET模式一样的问题。需要将recv/write函数设置成非阻塞形式。

  • LT模式支持非阻塞和阻塞读取模式。
  • select与poll也是以LT模式进行工作的。

因为ET模式不需要重读通知上层数据就绪,所以ET模式比LT模式工作效率高。

设置epoll ET工作模式只需要将event事件|EPOLLET宏即可

6. *demo 实例:Reactor模式ET工作epoll服务器实现简单的加法任务并写回

声明:为例防止粘包问题,这里选择将每个数据包之间用 | 来进行区分(制定协议)。

Github
Gitee

封装套接字sock.h

#pragma once

#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<string.h>

namespace NetWork_Socket{
  class Sock{
    public:
      static int Socket(){
        //创建监听套接字
        int listenSock=socket(AF_INET,SOCK_STREAM,0);
        if(listenSock<0){
          std::cout<<"socket error"<<std::endl;
          exit(-1);
        }
        int opt=1;
        //设置套接字属性
        setsockopt(opt,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
        return listenSock;
      }

      static bool Bind(int listenSock,int port){
        //绑定,IP=INADDR_ANY
        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(listenSock,(struct sockaddr*)&local,sizeof(local))<0){
          std::cout<<"bind error"<<std::endl;
          exit(-2);
        }
        return true;
      }

      static int Listen(int listenSock,int Len){//全连接队列长度
        //监听
        if(listen(listenSock,Len)<0){
          std::cout<<"listen error"<<std::endl;
          exit(-3);
        }
        return true;
      }
  };
}

Reactor设计模式,实现epoll任务派发功能。当任务就绪时采用回调机制执行对应的回调方法
epoller.h

#pragma once

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

namespace EpollSever{

#define MAXSIZE 64

  class epoller;
  class EventItem;

  typedef int(*call_back)(EventItem*);//函数指针回调函数 const& 输入 *(指针)输出 &输入输出

  class EventItem{
    public:
      int sock=0;
      //回指epoll模型
      epoller* Epoller=nullptr;
      //回调函数,用来进行数据处理
      call_back recv_hander=nullptr;
      call_back send_hander=nullptr;
      call_back error_hander=nullptr;

      //为了保证数据读取完毕,需要定义缓冲区
      std::string inbuff;//输入缓冲区
      std::string outbuff;//发送缓冲区

      void RegisterCallBack(call_back _recv,call_back _send,call_back _error){//管理回调函数,删除回调函数,只需要将参数设置为nullpt
        recv_hander=_recv;
        send_hander=_send;
        error_hander=_error;
      }

  };

  class epoller{
    private:
      int epfd;
      std::unordered_map<int,EventItem> event_item;//sock映射到EventItem,每个sock都有自己独立的缓冲区和处理方法
    public:
      epoller(){
        epfd=epoll_create(256);
        if(epfd<0){//256无意义->
          std::cerr<<"epoll create error"<<std::endl;
          exit(4);
        }
        std::cout<<"creat epoll fd="<<epfd<<std::endl;
      }

      void DisPatch(int timeout){// 如果底层特定事件就绪,就把特定事件分派给回调函数统一处理,称为事件分派器
        struct epoll_event fd_events[MAXSIZE];//MAXSIZE不能额大于创建epoll时的size及256
        int num=epoll_wait(epfd,fd_events,MAXSIZE,timeout);
        //内核会将就绪事件依次放入数组中,不会做重复遍历
        
        //std::cout<<"就绪事件数:"<<num<<std::endl;
        for(int i=0;i<num;i++){
          int sock=fd_events[i].data.fd;
          if((fd_events[i].events&EPOLLERR)||(fd_events[i].events&EPOLLHUP)){
            //对端异常或者连接断开->
            if(event_item[sock].error_hander!=nullptr){
              //将事件设置为读写关心
              fd_events[i].events=EPOLLIN|EPOLLOUT;
              event_item[sock].error_hander(&event_item[sock]);
            }
          }
          if(fd_events[i].events&EPOLLIN){
            //读事件就绪
            if(event_item[sock].recv_hander!=nullptr){//读回调函数存在
              event_item[sock].recv_hander(&event_item[sock]);//这个套接字独有的缓存区。
            }
          }
          if(fd_events[i].events&EPOLLOUT){
            //写事件就绪
            if(event_item[sock].send_hander!=nullptr){
              event_item[sock].send_hander(&event_item[sock]);
            }
          }
        }
      }

      ~epoller(){
        if(epfd>=0){
          close(epfd);
        }
      }
    
      void AddEvent(int sock,uint32_t event,EventItem&item){
        struct epoll_event ev;
        memset(&ev,0,sizeof(ev));
        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{
        //  std::cout<<"add sock "<<item.sock<<std::endl;
          event_item.insert(std::make_pair(sock,item));
        }
      }

      void DelEvent(int sock){
        if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr)<0){
          std::cout<<"epoll ctl del error"<<std::endl;
        }
        std::cout<<"remove sock "<<sock<<std::endl;
        event_item.erase(sock);
      }

      void EnableReadWrite(int sock,bool read,bool write){//之前没有关心过套接字的写,需要套接字写时调用这个接口修改成写即可
        //修改让epoll关注这个套接字的读写->
        struct epoll_event event;
        event.data.fd=sock;
        event.events=(read==true?EPOLLIN:0)|(write==true?EPOLLOUT:0)|EPOLLET;//ET工作模式选择读写
        if(epoll_ctl(epfd,EPOLL_CTL_MOD,sock,&event)<0){
          std::cerr<<"epoll ctl mod error ,sock:"<<sock<<std::endl;
        }
      }
  }; 
}

实现服务器自定义报文解析和设置非阻塞套接字等基本功能的头文件
Util.h

#pragma once 

#include<iostream>
#include<unistd.h>
#include<fcntl.h>
#include<vector>
#include<string>

//设置文件描述符为非阻塞

namespace Util{
  void SetNoBlock(int sock){//将套接字设置非阻塞
    int fd=fcntl(sock,F_GETFL);
    fcntl(sock,F_SETFL,fd|O_NONBLOCK);
  }

  void SplitStr(std::string&in,std::vector<std::string>&buff,std::string gist){
    //asdc|derc|代表两个完整的报文,序列化报文
    while(true){
      size_t pos=in.find(gist);
      if(pos==std::string::npos){
        break;
      }
      std::string ms=in.substr(0,pos);
      buff.push_back(ms);
      in.erase(0,pos+gist.size());//从头开始截取pos+gist.size()个
    }
  }

  void Deserialize(std::string& in,int&x,int&y){
    //in 认为字符串风格为1+1,不考虑错误情况
    int pos=in.find("+");
    x=atoi(in.substr(0,pos).c_str());
    y=atoi(in.substr(pos+1).c_str());//跳过加号
  }
}

epoller事件就绪时的各种回调函数头文件
app_interface.h

#pragma once 

#include<iostream>
#include"epoller.h"
#include"Util.h"
#include<vector>
#include<string>

namespace interface{

  using namespace EpollSever;

  int recver(EventItem*);
  int sender(EventItem*);
  int error(EventItem*);

  int accepter(EventItem*item){
    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){//底层没有连接了

          //std::cout<<"no link need to accept"<<std::endl;
          return 0;
        }
        if(errno==EINTR){
          //std::cout<<"interrupted by a signal"<<std::endl;
          //读取过程被信号打断
          continue;
        }
        else{
          //读取出错
          item->error_hander(item);
          return -1;
        }
      }
      else{
        std::cout<<"get a new link sock="<<sock<<std::endl;
        //设置非阻塞
        Util::SetNoBlock(sock);
        //读取成功,添加到epoller中,并添加这个套接字的注册方法
        EventItem tmp_item;
        tmp_item.sock=sock;
        tmp_item.Epoller=item->Epoller;
        tmp_item.RegisterCallBack(recver,sender,error);
        epoller* tmp_Epoller=item->Epoller;
        tmp_Epoller->AddEvent(sock,EPOLLIN|EPOLLET,tmp_item);
       // std::cout<<"add done"<<std::endl;
      }
    }

    return 0;
  }

  int recv_sock(int sock,std::string&out){
    //返回0 读取成功,-1读取失败
    while(true){
      char buff[1024]={0};
      ssize_t size=recv(sock,buff,sizeof(buff)-1,0);
      if(size<0){
        if(errno==EAGAIN||errno==EWOULDBLOCK){
          //读取完毕->
          return 0;
        }
        else if(errno==EINTR){
          //被信号中断
          continue;
        }
        else{
          //读取出错
          return -1;
        }
      }
      else{
        buff[size]=0;
        out+=buff;//将读取到的内容添加到inbuff中
      }
    }
  }

  int recver(EventItem*item){
    //数据读取
    //1.非阻塞读取
   // std::cout<<"recver redy"<<item->sock<<std::endl;
    if(recv_sock(item->sock,item->inbuff)<0){
      //读取失败->
      item->error_hander(item); 
      return -1;
    }
    //std::cout<<"client# "<<item->inbuff<<std::endl;
    //2.根据发来的数据流进行分包,防止粘包,涉及到协议定制,约定以|标定报文之间的分割符
    std::vector<std::string>MessArray;
    Util::SplitStr(item->inbuff,MessArray,"|");

    //std::cout<<item->inbuff<<std::endl;
    //3.针对每个报文,进行协议反序列化,这里要处理加法运算
    struct Date{
      int x=0;int y=0;
    };
    for(auto&mes:MessArray){
      std::cout<<mes<<std::endl;
      struct Date date;
      //将a+b字符串反序列化成 struct Date{int x=a;int y=b;}; 
      Util::Deserialize(mes,date.x,date.y);
      //std::cout<<date.x<<":"<<date.y<<std::endl;
      //4.业务处理,可以与线程池拓展处理,这里不考虑(构建任务类,将任务插入到线程池中)
      int ret=date.x+date.y;
      //5.形成响应报文,协议序列化成字符串
      std::string respon;
      respon+=std::to_string(date.x);
      respon+="+";
      respon+=std::to_string(date.y);
      respon+="=";
      respon+=std::to_string(ret);
      //添加响应报文的分隔符
      respon+="|";
      //6.写回
      //添加到输出缓冲区上
      item->outbuff+=respon;
    }
    //修改文件描述符的写
    if(!item->outbuff.empty()){
      item->Epoller->EnableReadWrite(item->sock,true,true);//关心这个文件描述符的读写
    }
    return 0;
  }

  //ET模式一次全部写完
  //返回0代表写完了,1代表没写完,下次继续写,-1写入失败
  int sender_sock(int sock,std::string&in){
    size_t total=0;//当前写入的字数,//不能直接全部发出,因为对端可能不能一次全部接受。
    while(true){
      size_t size=send(sock,in.c_str()+total,in.size()-total,0);
      if(size>0){
        total+=size;
        if(total>=in.size()){
          //写完了
          return 0;
        }
      }
      else if(size<0){
        if(errno==EAGAIN||errno==EWOULDBLOCK){
          in.erase(0,total);
          return 1;//对端无法再接受,但是写成功了,需要将发送缓冲区移除所有已经发送的数据->
        }
        else if(errno==EINTR){//被信号中断
          continue;
        }
        else{
          //写入失败
          return -1;
        }
      }
    }
  }

  int sender(EventItem*item){
    //发送数据
    int ret=sender_sock(item->sock,item->outbuff);
    if(ret==0){
      //发送完毕,不在关心这个文件描述符的写
      item->Epoller->EnableReadWrite(item->sock,true,false); 
    }
    else if(ret==1){
      //还没有全部发送完,继续关注这个文件描述符的读写,虽然这里默认文件描述符已经被设置了关心读写,这里为了保险再设置一次
      item->Epoller->EnableReadWrite(item->sock,true,true);
    }
    else{
      //出错
      item->error_hander(item);
    }
    return 0;
  }

  int error(EventItem*item){
    //出错
    //此时文件描述符被设置成了可读可写,可自行返回错误响应
    close(item->sock);
    item->Epoller->DelEvent(item->sock);
    return 0;
  }
}

启动服务器,并将监听套接字设置到epoll模型中,循环执行epoll任务分派函数
sever.cpp

#include"epoller.h"
#include<stdlib.h>
#include"sock.h"
#include"app_interface.h"
#include"Util.h"

const int Back_Log=5;

void UsrHelp(const char*name){
  std::cout<<"UsrHelp# "<<name<<" +port"<<std::endl;
}

int main(int argc,char*argv[]){
  if(argc!=2){
    UsrHelp(argv[0]);
    exit(5);
  }
  int port=atoi(argv[1]);

  //创建listen sock 
  int listen_sock=NetWork_Socket::Sock::Socket();

  std::cout<<"listen sock="<<listen_sock<<std::endl;

  //设置非阻塞
  Util::SetNoBlock(listen_sock);

  NetWork_Socket::Sock::Bind(listen_sock,port);
  NetWork_Socket::Sock::Listen(listen_sock,Back_Log);

  //调用epollsever事件管理器
  EpollSever::epoller epoll;

  EpollSever::EventItem item;
  item.sock=listen_sock;
  item.Epoller=&epoll;

  //这里只关注读事件,注册回调函数
  item.RegisterCallBack(interface::accepter,nullptr,nullptr);
 
  //将监听套接字托管到epoller,ET模式工作
  epoll.AddEvent(listen_sock,EPOLLIN|EPOLLET,item);

  int timeout=1000;
  while(true){
    epoll.DisPatch(timeout);
  }

  return 0;
}

在这里插入图片描述

7. Reactor设计模式(反应堆模式)

在这里插入图片描述
其中,还可以对Reactor模型进行拓展。即Reactor只负责读取数据流,将报文和报文进行分离,后续数据处理交给线程池。
这就是Reactor半同步半异步的工作方式,是Linux中最常用的工作方式。

后端开发就是在上述服务器代码的 业务处理开始到发送响应这块范围进行工作的。/(ㄒoㄒ)/~~

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NUC_Dodamce

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值