select poll epoll的一些比较
select的fd_set通过bitmap1024位表示存入的文件描述符,通过01表示存入的文佳描述符,且是从0下标开始,如存入的文件描述符是12579,则在bitmap里表示是0110010101000...
由于bitmap从0下标开始,存入的文件描述符从1开始,从0到存入的最大文件描述符,范围是max+1
select主要是把bitmap从用户态拷贝到内核态,由内核态来判断,没有数据变化,select会阻塞,有变化,bitmap的fd会被置位
fd_set不可重用
poll
struct pollfd
{
int fd;
short events;//事件
short revents;//有变化被置位,什么事件,置为什么事件,最后要将revents重新置为0
}
epoll句柄,用户态核内核态共享
select
关于select,主要是把服务端的根sockfd也加入到fd_set,FD_SET(listenSocket,&socketSet); readSet=socketSet;
writeSet=socketSet; 把加入的fd描述符更新到读写set中
FD_ZERO(&socketSet);
//将文件描述符放入 socketSet,
//用于accept
FD_SET(listenSocket,&socketSet);
//统计最大的socket
int maxfd = listenSocket;
int conNum = 1;
//数组存储连接的socket
int connectArray[1024]={0};
while(true)
{
//清空读写集合
FD_ZERO(&readSet);
FD_ZERO(&writeSet);
//读写都监听
readSet=socketSet;
writeSet=socketSet;
有新连接时:FD_SET(connectArray[conNum],&socketSet);放到socketSet后面while又更新到read_set
if(FD_ISSET(listenSocket,&readSet))
{
acceptSocket=accept(listenSocket,(sockaddr*)&addr,&len);
if(acceptSocket==INVALID_SOCKET)
{
return false;
}
else
{
//大于我们最大的监听数量了
if(conNum > 1024)
{
return false;
}
//更新数组
connectArray[conNum] = acceptSocket;
//设置非阻塞
fcntl(connectArray[conNum], F_SETFL, O_NONBLOCK );
//加到socketset里,以后赋值给读写集合
FD_SET(connectArray[conNum],&socketSet);
if(acceptSocket > maxfd)
{
maxfd = acceptSocket;
}
conNum++;
poll
poll与select不同在于:poll将要监听的对象存到数组中,且数组中的元素是结构体(存fd,要监听的事件,返回监听到的事件)
epoll
epoll查找基于红黑树,返回已经变化的文件描述符
一开始就监听服务端的fd(把服务端的fd事件加到epoll的监听树上(水平模式,只要有连接就通知服务端读事件)(监听读事件)),如果有客户端要建立连接,会触发epoll读事件
epoll_wait()接着通过for循环判断读事件结构体事件的fd是否是服务器的fd,若是就要建立连接的客户端数量:通过accept()接收客户端的连接,创建与客户端连接的sockfd,(边缘模式,非阻塞,加入epoll树,)epoll只是通知有事件要处理,具体处理要具体实现。;;若是触发epoll读事件的是要与客户端通信的sockfd,则在epoll_wait()数量的for里判断,进行通信
epoll边缘模式中,有要建立连接的客户端请求,将建立连接的sockfd1的结构体的事件设为读事件同时为边缘模式
connfd=accept(...);
event.data.fd = connfd;
event.events = EPOLLIN |EPOLLET;//读与边缘(服务端与客户端通信的fd的事件)
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
若是在已建立连接的客户端与服务端进行通信,由于服务端的sockfd是边缘模式,要一次性读完;同时读完后,服务端要与该客户端进行通信的connfd改为写事件(读完客户端的信息后,要进行回应,改为写边缘),服务端有数据发送回客户端触发写事件,一次性写完。
//更改为写模式
event.data.fd = connfd;
event.events = EPOLLOUT | EPOLLET;
//EPOLL_CTL_MOD,根据已有的connfd,修改事件结构体
epoll_ctl(epollfd, EPOLL_CTL_MOD, connfd, &event);
边缘模式下accept() do{accept(); 读完退出}while(1)要一次读完,直到返回值为<=0
边缘模式,写缓冲区满后,不写,contine 下一个epoll_event,下一次epoll_wait()写缓冲区变为非空后会触发写事件。
int nready = epoll_wait(epollfd, epoll_eventsList, eventsize, -1);
举一个例子假设epoll_event队列中有1000个文件描述符,第一次调用
epoll_wait返回5,那么表示队列前五个元素就绪了,如果不处理第三个就绪事件,其他的都处理。第二次调用epoll_wait会返回8,那么这8个epoll_event也是按顺序排列的。
所以认为epoll_wait这个函数做了内部的优化排序,返回给用户按顺序拍好的内存
epoll_wait最多返回eventsize个epoll_event
考虑把服务端的根sockfd设为水平模式,只要accept()里还有连接就触发,不用在do里全部读完accept()
epoll的一个示例:
#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<vector>
#include<arpa/inet.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<cstring>
#include<unistd.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<memory.h>
#include<errno.h>
#define SIZE 20
using namespace std;
int main(int argc,char* argv[])
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serveraddr;
memset(&serveraddr,0,sizeof(serveraddr));
serveraddr.sin_family=AF_INET;
serveraddr.sin_port=htons(atoi(argv[1]));
serveraddr.sin_addr.s_addr=INADDR_ANY;
if(bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0)
{
perror("bind error");
exit(1);
}
if(listen(sockfd,128)<0)
{
perror("listen error");
exit(1);
}
int epollfd=epoll_create(1);//创建epoll句柄
struct epoll_event event;
event.data.fd=sockfd;//将服务端的fd放到epoll中
event.events=EPOLLIN;//水平读
epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&event);//事件加入到epoll树
vector<int> clientfds;
struct epoll_event* ev=(struct epoll_event*)malloc(sizeof(struct epoll_event)*SIZE);//存epoll树返回的事件
struct sockaddr_in client;
int connfd;
socklen_t len=sizeof(client);
while(1)
{
int nearby=epoll_wait(epollfd,ev,SIZE,-1);//等待事件触发
if(nearby==-1)//出错
{
if(errno==EINTR) continue;
}
if(nearby==0)//超时
{
continue;
}
for(int i=0;i<nearby;i++)
{
if(ev[i].data.fd==sockfd)//有新连接
{
connfd=accept(sockfd,(struct sockaddr*)&client,&len);
char buff[1024]={0};
inet_ntop(AF_INET,&client.sin_addr.s_addr,buff,sizeof(buff));
cout<<"new connect:"<<buff<<endl;
clientfds.push_back(connfd);
event.data.fd=connfd;
event.events=EPOLLIN|EPOLLET;//读加边缘模式,读完后改为写
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
}else if(ev[i].events&EPOLLIN)//读事件
{
connfd=ev[i].data.fd;
char buff[1024];
int ret;
do{
memset(buff,0,sizeof(buff));
ret=read(connfd,buff,sizeof(buff));
if(ret<=0) break;//读完,退出
puts(buff);
}while(1);
event.data.fd=connfd;
event.events=EPOLLOUT|EPOLLET;//读完改为写
epoll_ctl(epollfd,EPOLL_CTL_MOD,connfd,&event);
}else if(ev[i].events&EPOLLOUT)//write
{
connfd=ev[i].data.fd;
const char* buff="1234566788qqwersda";
int ret;
do{
ret=write(connfd,buff,strlen(buff)+1);
if(ret<=0) break;//写完,退出
}while(1);
event.data.fd=connfd;
event.events=EPOLLIN|EPOLLET;//写完改为读
epoll_ctl(epollfd,EPOLL_CTL_MOD,connfd,&event);
}
}
}
}
epoll反应堆
epoll反应堆主要是读完数据后,改为写事件触发
主要是利用epoll_data的void*ptr该指针指向一个自定义结构体,该结构体存fd,events,回调函数
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
因此监听的对象的fd在自定义的结构体上
将void*ptr指向自定义的结构体的epoll_event上树,后面调用epoll_wait(),返回的 epoll_event结构体是在树上的结构体的复制体(将变化的节点拷贝下来)
.........