多路转接之epoll
- 一、epoll模型的底层原理
- 1.数据从外设到cpu的过程
- 2.epoll底层的数据结构
- 3.epoll的两种工作模式
- 二、认识epoll
- 1.epoll_create
- 2.epoll_ctl
- 3.epoll_wait
- 三、基于epoll实现的server
- main.cc
- epollServer.hpp
- sock.hpp
- Log.hpp
- public.hpp
一、epoll模型的底层原理
1.数据从外设到cpu的过程
网络中的数据先到网卡外设,数据到达网卡,网卡会向cpu发送硬件中断,cpu根据中断信号在中断向量表找到中断对应的中断处理函数方法,这个函数方法就会让网卡驱动程序将数据从网卡拷贝到内存,cpu执行指令时再从内存中读取数据。
2.epoll底层的数据结构
epoll是操作系统提供的系统调用,操作系统在创建epoll时,维护了红黑树,每个红黑树结点都包含了用户告诉操作系统需要监视的文件描述符的事件。一个结点对应一个描述符的一个事件,每个结点包含(int sockfd,uint32_t events)这些信息。
操作系统在创建epoll时还创建了一个就绪队列,就是一个双向链表,每个链表结点包含(int sockfd,uint32_t events)这些信息,就绪队列中存储事件就绪的文件描述符和事件,就绪队列作用就是操作系统内核告诉用户哪些文件描述符的哪些事件就绪了。
数据从网卡经过网络协议栈到达文件的缓冲区,此时文件缓冲区有数据就绪,文件中有对应的回调函数(void*private_data)会将红黑树中的文件描述符事件对应的结点加入就绪队列中。
红黑树,双向链表以及回调方法组成了epoll模型。这个epoll模型被放在一个struct file文件中,所以epoll模型也有自己的文件描述符,epoll模型对应的文件描述符被放在进程PCB中的文件描述符表中所管理。
3.epoll的两种工作模式
epoll的两种工作模式:LT(Level Triggered)水平触发、ET(Edge Triggered)边缘触发。
LT:只要传输层的接受缓冲区里面有数据,epoll就会一直通知用户读取数据。或者传输层的发送缓冲区有剩余空间,就一直通知用户写数据。
ET:文件描述符对应的事件就绪,epoll只会通知用户一次。接受缓冲区有数据到来,会通知用户一次,如果接受缓冲区里的数据没有读取完,epoll就不会通知用户读取数据,除非接受缓冲区的数据增加了。同理发送缓冲区有空间,操作系统内核只通知用户一次。
select,epoll默认工作模式是LT。
ET模式下文件描述符必须是非阻塞的,因为为了保证将接受缓冲区的数据读完,必须采用循环读取的策略,当读到没有数据时,如果文件描述符是阻塞的,那么进程就会被阻塞。所以必须保证文件描述符是非阻塞的,在非阻塞状态下,若文件描述符对应的文件中没有数据,recv会返回-1,errno会被设置成EAGAIN或EWOULDBLOCK。
LT模式下文件描述符既可以是阻塞的也可以是非阻塞的,当是非阻塞的时候和ET模式工作效率没有区别。
ET比LT更高效是因为ET只通知一次,用户必须将接受缓冲区的数据全部取走,或者将数据写入发送缓冲区直到数据写完或者没有空间。将接受缓冲区的数据全部取走,这样可以通过tcp向对方发送更大的窗口大小,对方的滑动缓冲区根据窗口大小也会变大,滑动缓冲区变大就能一次型发送更多的数据。从而提高网络通信的效率。
TCP中的PSH类型报文,就是将数据就绪事件再次让用户知道。
二、认识epoll
1.epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
epoll_create:创建epoll模型。
size必须大于0,返回值是epoll模型对应的文件描述符。
2.epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};
epoll_ctl:对epoll模型中的红黑树做增删查改。红黑树是以文件描述符作为key值的。
用户告诉内核需要监视文件描述符的事件。
op代表用户想要怎样修改红黑树。
//op的取值
EPOLL_CTL_ADD //增加epoll监视的事件
EPOLL_CTL_MOD //修改epoll监视的某个事件
EPOLL_CTL_DEL //删除epoll事件的某个事件
3.epoll_wait
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
epoll_wait:从epoll模型中的就绪队列提取就绪文件描述符的事件。
就绪队列保证了无需检测文件描述符的事件是否就绪。
返回值表示就绪事件的数量。
events表示事件可能的取值为:
EPOLLIN:表示对应的文件描述符可以读;
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数可读;
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: ET的epoll工作模式;
三、基于epoll实现的server
main.cc
#include "epollServer.hpp"
#include <memory>
using namespace std;
using namespace epoll_ns;
std::string handle(const std::string& request){return "I am epollServer,"+request;
}
static void usage(string proc){cerr<<proc<<" need a port!!!!"<<endl;
}
int main(int argc,char* argv[]){if(argc!=2){usage(argv[0]);exit(1);}uint16_t port=atoi(argv[1]);unique_ptr<epollServer> svr(new epollServer(handle,port));svr->init();svr->start();return 0;
}
epollServer.hpp
#pragma once
#include <iostream>
#include "sock.hpp"
#include "public.hpp"
#include <errno.h>
#include <cstring>
#include <functional>
#include <sys/epoll.h>
#include <functional>
using namespace std;
namespace epoll_ns{class epollServer{static const uint16_t defaultPort=8080;static const int defaultfd=-1;static const int size=128;static const int defaultnum=64;using func_t=std::function<std::string(const std::string&)>;public:epollServer(func_t func,uint16_t port=defaultPort,int num=defaultnum):_func(func),_revs(nullptr),_num(num),_port(port),_listenSockfd(defaultfd),_epollfd(defaultfd){}~epollServer(){if(_listenSockfd!=-1)close(_listenSockfd);if(_epollfd!=-1)close(_epollfd);if(_revs!=nullptr)delete[] _revs;}void init(){_listenSockfd=Sock::Socket();Sock::Bind(_listenSockfd,_port);Sock::Listen(_listenSockfd);//创建epoll模型_epollfd=epoll_create(size);if(_epollfd==-1){log(FATAL,"epoll_create error:%s",strerror(errno));exit(EPOLL_CREATE_ERR);}//将监听套接字的读事件加入到epoll中struct epoll_event ev;ev.events=EPOLLIN;ev.data.fd=_listenSockfd;epoll_ctl(_epollfd,EPOLL_CTL_ADD,_listenSockfd,&ev);//初始化就绪事件的数组_revs=new struct epoll_event[_num];log(NORMAL,"epellserver init sucess!!!" );}void handlerEvent(int readyNum){std::cout<<"hanlder in"<<std::endl;for(int i=0;i<readyNum;++i){int sock=_revs[i].data.fd;uint32_t event=_revs[i].events;if(sock==_listenSockfd&&(EPOLLIN&event)){string clientIp;uint16_t clientPort;int fd=Sock::Accept(sock,&clientIp,&clientPort);if(fd==-1){log(NORMAL,"accept error!!");continue;}//将连接套接字文件符的读事件加入epoll中struct epoll_event ev;ev.data.fd=fd;ev.events=EPOLLIN;epoll_ctl(_epollfd,EPOLL_CTL_ADD,fd,&ev);}//普通文件描述符的读事件就绪else if(EPOLLIN&event){char buffer[1024];//可能没读取完int n=recv(sock,buffer,sizeof(buffer),0);if(n<0){epoll_ctl(_epollfd,EPOLL_CTL_DEL,sock,nullptr);close(sock);log(ERROR,"recv error,errno=%d,strerror=%s",errno,strerror(errno));}else if(n==0){epoll_ctl(_epollfd,EPOLL_CTL_DEL,sock,nullptr);close(sock);log(ERROR,"link close");}else{buffer[n]=0;cout<<"receive a message:"<<buffer<<endl;string request=buffer;std::string response=_func(request);write(sock,response.c_str(),response.size());}}}std::cout<<"handler out"<<std::endl;}void start(){int timeout=-1;while(true){int n=epoll_wait(_epollfd,_revs,_num,timeout);switch(n){case -1:log(ERROR,"epoll_write is error!!! errno=%d,strerror=%s",strerror(errno));return;break;case 0:std::cout<<"time out..."<<std::endl;log(NORMAL,"timeout...");break;default:handlerEvent(n);log(NORMAL,"have events ready!!!");break;}}}private:uint16_t _port;int _listenSockfd;int _epollfd;func_t _func;struct epoll_event* _revs;int _num;};
}
sock.hpp
#pragma oncce
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <sys/wait.h>
#include "Log.hpp"
#include "public.hpp"
using namespace std;class Sock{static const int backlog=5;
public://创建套接字文件描述符static int Socket(){int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd==-1){log(ERROR,"create a socket error");exit(SOCKET_ERR);}else log(NORMAL,"create socket success, sockfd=%d",sockfd);//地址复用int opt=1;setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));return sockfd;}//给套接字绑定端口号static void Bind(int sockfd,uint16_t port){struct sockaddr_in local;socklen_t len=sizeof(local);bzero(&local,len);local.sin_family=AF_INET;local.sin_addr.s_addr=INADDR_ANY;local.sin_port=htons(port);int n=bind(sockfd,(const struct sockaddr*)&local,len);if(n==-1){log(ERROR,"bind sockfd error");exit(BIND_ERR);}else log(NORMAL,"bind sockfd success");}//监听套接字static void Listen(int sockfd){int n=listen(sockfd,backlog);if(n==-1){log(ERROR,"listen sockfd error");exit(BIND_ERR);}else log(NORMAL,"listen sockfd success");}//获取新连接套接字static int Accept(int listenSockfd,string* clientIp,uint16_t* clientPort){struct sockaddr_in client;socklen_t len=sizeof(client);bzero(&client,len);int sockfd=accept(listenSockfd,(struct sockaddr*)&client,&len);if(sockfd==-1)log(ERROR,"accept sockfd error");else {log(NORMAL,"accept a link socket=%d",sockfd);*clientIp=inet_ntoa(client.sin_addr);*clientPort=ntohs(client.sin_port);}return sockfd;}
};
Log.hpp
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <cstdio>
#define NORMAL 0
#define DEBUG 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
using namespace std;
class LogMessage{
private:string log_path="./log.txt";string err_path="./err.txt";static string getLevel(int level){switch(level){case NORMAL:return "NORMAL";break;case DEBUG:return "DEBUG";break;case WARNING:return "WARNING";break;case ERROR:return "ERROR";break;case FATAL:return "FATAL";break;default:return "OTHER";}}string getTime(){time_t now=time(nullptr);struct tm* t=localtime(&now);int year=t->tm_year+1900;int mon=t->tm_mon+1;int day=t->tm_mday;int hour=t->tm_hour;int min=t->tm_min;int sec=t->tm_sec;char buffer[64];sprintf(buffer,"%d-%02d-%02d %02d:%02d:%02d",year,mon,day,hour,min,sec);return buffer;}
public:void operator()(int level,const char* format,...){string lev=getLevel(level);string time=getTime();va_list list;va_start(list,format);char msg[1024];vsnprintf(msg,sizeof(msg),format,list);FILE* log=fopen(log_path.c_str(),"a+");FILE* err=fopen(err_path.c_str(),"a+");if(log!=nullptr&&err!=nullptr){FILE* cur=nullptr;if(level==NORMAL||level==DEBUG){cur=log;}else cur=err;fprintf(cur,"[%s][%s][%s]\n",lev.c_str(),time.c_str(),msg);fclose(log);fclose(err);} }
};
static LogMessage log;
public.hpp
#pragma once
#include <iostream>
enum{SOCKET_ERR=1,BIND_ERR,LISTEN_ERR,ACCEPT_ERR,USAGE_ERR,EPOLL_CREATE_ERR};