epoll TcpMultiplexor笔记

来源:互联网 发布:盘搜搜源码 编辑:程序博客网 时间:2024/05/17 22:27

TcpMultiplexor
简单的epoll练习


void TcpMultiplexor::run()
{
m_epfd = epoll_create(10000);//最大连接数,epoll fd,epoll自身也是一个文件描述符,所以用完之后记得close
if(m_epfd == -1)
return;//出问题了
epoll_event *event = new epoll_event[10000];//事件队列
bzero(events,sizeof(epoll_event)*10000);//这块内存清干净
m_running = true;

while(m_running)
{
int nfds = epoll_wait(m_epfd,events,10000,15);//epoll句柄,事件队列,最大连接数,超时时间。
if(nfds>0)
{
 for(int i=0;i<nfds;i++)
 {
//说明有变化
TcpConnection* conn = static_cast(TcpConnection*)(events[i].data.ptr);//转下
//事件队列直接取,也就是说events里面的都是有变化的 
//我只要处理这些events 一个个处理就OK了
if(conn==null)
continue;
int event = events[i].events;//几个事件常量 看看是什么事件
if(event&EPOLLOUT)
{
//说明可以写数据 这个线程现在可以发数据了
int err = conn->onSend();
if(err == SOCKET_ERR)
remove(conn);//连接有问题
}
if(event&EPOLLIN)
{
//事件表明现在这条连接有数据可以读
int err = conn->onRecv();
//同样出错就remove
}
if(event&EPOLLING==0 && event&EPOLLOUT==0)
{
//其他事件,移除
remove(conn);
}
  }
}
}
delete[] events;
}
上面这个套路很固定,线程run,create一个epoll,
然后一直while,epoll_wait,发现有变化的events,然后挨个处理就行,event{事件,conn}
epoll_wait类似于select,epoll_wait中的events用来从内核得到事件集合,10000告诉内核这个events有多大,
当然不能大于epoll_create时候的max_size。epoll_wait返回需要处理的事件的数目,返回0表示超时
LT模式就是只要数据没有处理就一直会通知下去。
ET只有变化的时候才通知,不包括缓冲器还有未处理的数据


void TcpMultiplexor::add(TcoConnection* conn)
{
if(conn == NULL)
return;
epoll_event ev={};
ev.events = 0;//事件
ev.data.ptr = conn;//连接 (文件描述符fd)
int err = epoll_ctl(m_epfd,EPOLL_CTL_ADD,conn->getFd(),&ev);//epoll的fd,动作,连接fd,事件
//m_epfd的哪个epoll收到EPOLL_CTL_ADD类型的消息,说明是要有加入新的连接了。后面是这个连接的内容
//包括连接的句柄conn->getFd(),连接的回调函数。以后epoll就知道发生了什么事情,收到数据?发送数据?
//就会回调这个链接对应的函数,
//因此只要epoll_wait得到events事件队列,然后取出里面的事件,事件类型可以知道,对应的哪个连接也知道,
//因此就可以直接处理发送和读消息了,效率提升,select则需要遍历才能知道哪一个有变化
if(err == 0)
{
conn->setEpfd(m_epfd);//设置这个连接 收发数据 受m_epfd这个epoll的监视
con->onConnect();//正式连上,与epoll里面其他连接一样了,再收到或者发送数据 epoll就能感知到了
//那之前怎么连上的?之前只是acceptor到,收到监听了,这么多的连接如何管理,通过epoll管理
}
else
return;//出错
}


void TcpMultiplexor::remove(TcpConnection* conn)
{
if(NUll == conn)
return;
epoll_event ev = {};
epoll_ctl(m_epfd,EPOLL_CTL_DEL,conn->getFd(),&ev);
//给m_epfd的epoll发送消息,通知他有个连接要移除掉,要移除掉的连接的文件描述符,句柄是xxx,事件是xxx
m_conns.erase(conn->getFd());//移除
conn->onDisconnect();//断开
}


以上epoll_ctl注册事件,很重要,注册提供conn的文件描述符,提供一个event容器,发生事件了,
只要处理事件队列,就可以得到 哪个文件描述符(哪个连接)?发生了什么事情?,然后对应的文件描述符的conn来处理就OK
注册有三种动作 增改删,这个动作是epoll的注册动作
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DEL


每个conn的文件描述符收到的事件类型有以下几种
EPOLLIN 对应的文件描述符可读
EPOLLOUT 可写
EPOLLPRI 紧急数据
EPOLLERR 错误
EPOLLHUP 挂断
EPOLLET 设置为边缘触发,默认是水平触发
EPOLLONESHOT 一次性,如果需要继续监听这个socket,那么就需要把这个socket再加入到epoll队列中,
重新epoll_ctl注册EPOLL_CTL_ADD




epoll ET写法




epfd = epoll_create(256);//epoll的fd


listenfd = socket(AF_INET,SOCKET_STREAM,0);//端口监听socket,监听的这个socket的文件描述符是listenfd
ev.data.fd = listenfd; //回调 fd就是listenfd,事件就是读事件,ET边缘触发
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); //epoll_ctl注册这个listenfd文件描述符这条socket
//当这里有收到消息,那么就可以调用回调处理这条socket的读消息
bind(listenfd,(socketaddr*)&serveraddr,sizeof(serveraddr));
listen(listenfd,LISTENQ);
//这里开始监听,监听端口一收到消息我们的epoll回调就可以处理了
while(running)
{
nfds = epoll_wait(epfd,events,10000,15);//epfd就是等待的epoll,类似select,15s超时,events是有状态变化的socket的回调函数列表,10000最大连接数,
//nfds就是返回的有数目,有状态变化的socekt的数目
//循环处理 events[i]
for(int i=0;i<nfds,i++)
{
if(events[i].data.fd == listenfd)
{
//说明啥,说明新连接来了,赶紧accept,并加入epoll,加入后就是正式的连接了
confd = accept(listenfd,(sockaddr*)clientaddr,&clilen);//这里应该循环accept,否则这次没有accept的下次也收不到
if(confd<0)
{
//错误
}
ev.data.fd = confd;
ev.events = EPOLLIN|EPOLLET;//接收事件,ET触发
epoll_ctl(epfd,EPOLL_CTL_ADD,confd,&ev);//把这个刚accept过来的连接加入epoll,以后通过epoll的回调处理
}
else if(events[i].events&EPOLLIN)
{
//这里说明是一般的已加入的连接有可读消息
sockfd = events[i].data.fd;//如果<0说明有问题,回调事件的文件描述符怎么会小于0
n = read(sockfd,line,MAXLINE);
if(n<=0)
{
if(errno == ECONNRESET)
close(sockfd);
events.data.fd = -1;
//关掉连接,事件的fd设为-1,不然到时候找不到连接的文件句柄
}
else
{
ev.data.fd = sockfd;//句柄不变
ev.events = EPOLLOUT|EPOLLET;//读完了状态改为写 还是边缘触发
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//因此epoll_ctl注册修改事件,改下这个fd的回调
}
}
else if(events[i].events&EPOLLOUT)
{
sockfd = events[i].data.fd;
write(sockfd,line,n);
ev.data.fd = sockfd;
ev.events = EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//改这个socket的事件为读
}


}
}


socket阻塞还是不阻塞
setnonblock(m_fd)//m_fd是一个socket的文件句柄
setnodelay(m_fd)//nagle
setkeepalive(m_fd,300);//300ms


epoll_wait(m_epfd,events,10000,15); 
最后一个0 表示不等待。非阻塞,大于1表示阻塞,-1表示等到为止
阻塞socket,read write -1,表示网络错了,非阻塞-1表示不一定真的出错了,可能只是暂时不可用。


因此对于非阻塞的non_blocking的socket,errno==EAGAIN,
那么下次就应该继续读继续写。
non_blocking-----LT,errno==EAGAIN,继续读写




LT模式没有问题,因为只要还有未处理的数据events里面就有这个socket的回调事件
ET模式则不行,状态没有发生变化,从events里面你压根就得不到
non_blocking-----ET,就需要一直读写,直到没有可读可写为止,否则下次你就得不到了
ET需要不停地改变注册事件



一般使用 阻塞LT模式最安全。
xx里面的accept没有放在epoll里面是好样的
xx里面是LT,每次读里面有个while 读就读完,写就写完
然后处理完这次的events,下次epoll_wait再处理下次的events,如果是ET模式那么这次没弄完的下次就没有触发事件了
非阻塞 + ET + 一直while读写完
非阻塞 + LT + 下次可继续


阻塞 + LT + 每次while全部读写完 = 最安全
ET模式下每次处理,需要while处理


监听accept套接字设为非阻塞,读写数据的socket为阻塞模式
accept的处理也要循环,因为当多个同时连接server,结果只accept一个,其他的没处理,下次也没有这些未处理的事件


while循环包住accept的调用,处理完TCP就绪队列中所有连接再退出循环
bin listen accept(while)


EAGAIN EWOULDBLOCK EINTR




阻塞 不立即返回,取到了再返回,所以会阻塞住
非阻塞 取到娶不到都立即返回,隔段时间取一下,隔断时间取一下,所以我没有被阻塞,不用等待,最后也能取到
阻塞非阻塞:等待与否、轮询与否
同步异步:通知与否


同步 阻塞非阻塞 都属于同步,IO操作的时候都会讲process阻塞,只是一个立即返回,一个等待。
异步 取数据,数据好了会通知我,我接到通知然后取,这就是异步,当然异步不会阻塞了,因为我不需要等,对方会自己通知我


非阻塞是轮询取,娶不到返回,取到进程还是被block
异步则是不管怎么都是直接返回不理睬。


异步不需要自己拷贝数据
阻塞非阻塞都得自己主动拷贝数据


原创粉丝点击