Reactor模式-----基于Epoll实现

Reactor模式-----基于Epoll实现

Reactor模式

该模式要求主线程只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程。除此之外,主线程不做任何实质性的工作。读写数据,接受新的连接以及处理客户请求均在工作线程中完成。
使用同步I/O模型(以epoll_wait为例)实现的Reactor模式的工作流程是:

  1. 主线程往epoll内核时间表中注册socket上的读就绪事件。
  2. 主线程调用epoll_wait等待socket上有数据可读。
  3. 当socket上有数据可读时,epoll_wait通知主线程,主线程则将socket可读事件放入请求队列。
  4. 睡眠在请求队列上的某个工作线程被唤醒,他从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket的写就绪事件。
  5. 主线程调用epoll_wait等待socket可写。
  6. 当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。
  7. 睡眠在请求队列上的某个工作线程被唤醒,他往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的时候指定了端口,但是最后运行起来的时候却不是我指定的端口,且固定为另一个端口,但是还可以正常运行,还不太清楚这里面是什么原因。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值