Reactor模式-----基于Epoll实现
Reactor模式
该模式要求主线程只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程。除此之外,主线程不做任何实质性的工作。读写数据,接受新的连接以及处理客户请求均在工作线程中完成。
使用同步I/O模型(以epoll_wait为例)实现的Reactor模式的工作流程是:
- 主线程往epoll内核时间表中注册socket上的读就绪事件。
- 主线程调用epoll_wait等待socket上有数据可读。
- 当socket上有数据可读时,epoll_wait通知主线程,主线程则将socket可读事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,他从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket的写就绪事件。
- 主线程调用epoll_wait等待socket可写。
- 当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,他往socket上写入服务器处理客户请求的结果。
基于Epoll的实现
本例没采用多线程。
#include "my_socket.h"
#include <time.h>
#include <sys/epoll.h>
#define MAX_EVENTS 1024 //监听上限,大于1024需要改打开文件个数限制
#define SERV_PORT 8001
//该结构体主要用于epoll中的结构体epoll_event.data.ptr,本身ptr是void*类型的,这里我们自定义该类型为myevent_s,同时为
//了方便把本来的epoll_event.events也一起封装到myevent_s中来。
struct myevent_s{
int fd; //要监听的文件描述符
int events; //对应的监听事件
void *arg; //泛型参数
void (*call_back)(int fd, int events, void*arg); //回调函数
int status; //是否在监听:1表示在红黑树上(监听),0表示不在红黑树上
//(不监听)
char buf[BUFSIZ];
int len;
long last_active; //记录每次加入红黑树g_efd的时间
};
void initlistensocket(int efd, short port);
void eventset(struct myevent_s* ev, int fd, void (*call_back)(int,int,void*),void *arg);
void eventadd(int efd, int events, struct myevent_s * ev);
void eventdel(int efd, struct myevent_s *ev);
void acceptconn(int lfd, int events, void *arg);
void recvdata(int fd, int events, void * arg);
void senddata(int fd, int events, void *arg);
//----------------------------------------------------初始化---------------------------------------------------
int g_efd; //红黑树树根,由epoll_create返回
struct myevent_s g_events[MAX_EVENTS+1]; //自定义结构体类型数组,最后加的结构体元素专门用来存放lfd对应的结构体
//----------------------------------------------------utils---------------------------------------------------
void eventset(struct myevent_s* ev, int fd, void(*call_back)(int,int,void*),void*arg){
ev->fd = fd;
ev->call_back = call_back;
ev->events =0;
ev->arg = arg;
ev->status = 0;
//memset(ev->buf, 0, sizeof(ev->buf)); 不能在这重置buf,因为在接收到数据之后要用eventset去设置写监听。
//ev->len = 0; 若重置buf,会导致读到的数据在eventset的时候丢失,没法写数据
ev->last_active = time(NULL);
return;
}
void eventadd(int efd, int events, struct myevent_s * ev){
struct epoll_event epv = {0,{0}}; //用于设置epoll事件的临时结构体的初始化,该结构体在epoll_ctl的manpage中有介绍
int op;
epv.data.ptr = ev; //普通epoll时,设置socket是通过data的fd设置的,在reactor中设置fd直接通过ptr设置指定的socket,\
此时不需要设置fd,因为我们把fd封装到我们自己的结构体中,不需要重复去设置epoll_event.data.fd
epv.events = events;
ev->events = events;
if(ev->status ==0){//未监听,这里置为1进行监听。
op = EPOLL_CTL_ADD;
ev->status = 1;
}
//原本在eventset中对buf和len的初始化应该在这里判断只有EPOLLIN才初始化,EPOLLOUT不初始化。
if(events == EPOLLIN){
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
}
if(epoll_ctl(efd, op, ev->fd, &epv)<0) //将我们已经设置好的event赋给临时的epoll_event再传给epoll_ctl把事件挂到树上监听
printf("event add failed [fd = %d], events[%d]\n", ev->fd, events);
else
printf("events add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op , events);
return;
}
void eventdel(int efd, struct myevent_s *ev){
struct epoll_event epv = {0,{0}};//用于进行epoll_ctl的临时结构体
if(ev->status != 1) // 说明该事件已经不在树上,已经不处于被监听的状态
return;
epv.data.ptr = NULL;
ev->status = 0;
epoll_ctl(efd,EPOLL_CTL_DEL, ev->fd, &epv);
}
void initlistensocket(int efd, short port){
struct sockaddr_in sin;
int lfd = Socket(AF_INET, SOCK_STREAM, 0);
int flag = fcntl(lfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(lfd,F_SETFL, flag);
memset(&sin , 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = SERV_PORT;
Bind(lfd, (struct sockaddr *)&sin, sizeof(sin));
Listen(lfd,128);
eventset(&g_events[MAX_EVENTS],lfd, acceptconn, &g_events[MAX_EVENTS]);//红黑树的最后一个元素存放lfd对应的event\
这里就只是个初始化工作,把lfd对应的事件,\
需要的回调函数,监听状态等等都设置好。
eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); //将设置好的event结构体(epoll_event.data.ptr)设置为监听并放到监听树上
return;
}
//----------------------------------------------------回调---------------------------------------------------
//(区别于以往回调函数的直接调用,本程序中多封装了一层结构体myevent_s,把回调函数封装到结构体中又或者回调函数中调用
//回调函数,所以看着有些复杂。理解调用关系之后就不难了。)
//该回调函数专用于lfd用于监听并为服务器和客户端建立新的socket连接。故该回调函数只在initlistencosket函数中
//进行eventset的时候会用到,绑定到结构体中,当需要的时候调用结构体的callback即相当于直接调用acceptconn。
//其他回调函数的调用方式本程序中也同理,先设置到event结构体的call_back回调函数,然后在需要的时候直接调结构体的call_back,
//相当于直接调用回调函数。
void acceptconn(int lfd, int events, void *arg){
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if((cfd = Accept(lfd , (struct sockaddr *)&cin, &len)) == -1 ){
//返回的已建立连接的connfd,加入到epfd监听红黑树中
if( errno == EAGAIN || errno == EINTR)
printf("accept is interrupted by signal!\n");
else
printf("%s:Accept, %s \n", __func__,strerror(errno));
return;
}
do{
for(i = 0; i< MAX_EVENTS; i++){ //从g_events中找一个空闲元素
if(g_events[i].status ==0)
break;
}
if( i == MAX_EVENTS){
printf("%s: max connent limit[%d]\n", __func__, MAX_EVENTS); //__func__函数名称,__LINE__行号
break;
}
int flag = 0;
if((flag = fcntl(cfd, F_SETFL, O_NONBLOCK))<0){
printf("%s: fcntl nonblocking failed , %s\n",__func__, strerror(errno));
break;
}
//对刚刚找到的g_events数组中的空的event进行设置,设置为当前cfd的接收事件
eventset(&g_events[i], cfd, recvdata, &g_events[i]);
eventadd(g_efd, EPOLLIN, &g_events[i]);
}while(0);//因为lfd监听事件一直开着,这里面用dowhile对于新的建立连接的请求一直做处理,
printf("new connect [%s:%d][time:%ld],posp%d]\n",
inet_ntoa(cin.sin_addr),ntohs(cin.sin_port),g_events[i].last_active,i);
return;
}
//该回调函数用于接收数据。本程序完成echo功能,故一般在acceptconn回调函数中被eventset设置并挂到树上
void recvdata(int fd, int events, void * arg){
struct myevent_s * ev = (struct myevent_s *)arg;//这里的arg对应其他调用函数中传过来的整个自定义的结构体myevent_s\
在本程序中由于完成回射,也就是echo的功能。\
因此这个函数是在被accept之后调用的,去读刚刚accept的socket中发来的数据\
要读数据,就要知道相应的fd等信息。在这里recvdata的函数参数有点\
冗余了,只需要第三个参数即可。但是因为eventset中对回调函数的参数定死了\
所以即使有冗余也没法改。
int len = recv(ev->fd, ev->buf, sizeof(ev->buf), 0);
eventdel(g_efd,ev); //对于刚刚accept的这个结构体,已经完成读监听,后面还需要去写监听,所以这里先将结构体从树上摘下来。
if(len>0){
ev->len = len;
//ev->buf[len] = '\0';
printf("Receive data from fd=[%d] : %s\n",ev->fd,ev->buf);
eventset(ev,ev->fd,senddata,ev); //接着同样是这个accept之后生成的socket对应的结构体,现在需要进行写监听,所以设置\
senddata 和 EPOLLOUT
eventadd(g_efd,EPOLLOUT,ev);
}
else if (len == 0){
close(ev->fd);
printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
}
else{
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s \n",ev->fd,errno,strerror(errno));
}
return;
}
void senddata(int fd, int events, void *arg){
struct myevent_s * ev = (struct myevent_s *) arg;
int len;
len = send(fd,ev->buf,ev->len,0);
eventdel(g_efd,ev);//此时已经完成写操作,所以把写监听从树上摘下
if (len>0){
printf("send[fd = %d], [%d]%s\n", fd ,len,ev->buf);
eventset(ev,fd,recvdata,ev);//已经往socket中完成写,并且确实写成功了(len>0),于是需要再为读监听做准备
eventadd(g_efd,EPOLLIN,ev);//设置读监听并挂到树上。
}
else{
close(ev->fd);
printf("send[fd=%d] error %s \n", fd, strerror(errno));
}
return;
}
int main(int argc,char *argv[]){
short port = SERV_PORT;
if(argc ==2)
port = atoi(argv[1]); //如果用户指定端口则使用指定端口否则用默认的8001
g_efd = epoll_create(MAX_EVENTS+1); //创建epoll的监听红黑树,树根为全局变量
if(g_efd <=0)
printf("create efd in %s err %s \n",__func__, strerror(errno));
initlistensocket(g_efd,port);
struct epoll_event events[MAX_EVENTS+1];
printf("server running:port[%d]\n",port);
int checkpos = 0,i;
while(1){
//每次测试100个连接是否超时,超时时间为60秒,如果超时则关闭连接
long now = time(NULL);
for(i = 0;i<100;i++,checkpos++){
if(checkpos == MAX_EVENTS)
checkpos = 0;
if(g_events[checkpos].status != 1)
continue;
long duration = now - g_events[checkpos].last_active; //查看不活跃的时长,如果超过60则关闭。
if(duration >= 60){
close(g_events[checkpos].fd);
printf("[fd=%d] timeout\n", g_events[checkpos].fd);
eventdel(g_efd, &g_events[checkpos]);
}
}
int nfd= epoll_wait(g_efd, events, MAX_EVENTS+1,1000);
if (nfd < 0){
printf("epoll_wait error , exit\n");
break;
}
for(i =0;i<nfd; i++){
struct myevent_s *ev = (struct myevent_s*) events[i].data.ptr;
if((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)){ //前一个判断的是epollwait监听到的事件是否为EPOLLIN\
,后一个判断的是我们当时对该events设置的事件是否\
监听的就是EPOLLIN,两个要对的上。
ev->call_back(ev->fd, events[i].events, ev->arg);
}
if((events[i].events & EPOLLOUT)&& (ev->events&EPOLLOUT)){
ev->call_back(ev->fd,events[i].events,ev->arg);
}
}
}
return 0;
}
这个实现代码还是有点问题,我在epoll_create的时候指定了端口,但是最后运行起来的时候却不是我指定的端口,且固定为另一个端口,但是还可以正常运行,还不太清楚这里面是什么原因。