实现epoll事件的两种模型(ET/LT)、epoll反应堆模型

前置知识:

多进程/线程并发服务器、多路I/O转接服务器的简单实现-CSDN博客

1. 事件模型

EPOLL事件有两种模型:

  • Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
  • Level Triggered (LT) 水平触发只要有数据都会触发。

思考如下步骤:

1. 假定我们已经把一个用来从管道中读取数据的文件描述符(rfd)添加到epoll描述符。

2. 管道的另一端写入了2KB的数据

3. 调用epoll_wait,并且它会返回rfd,说明它已经准备好读取操作

4. 读取1KB的数据

5. 调用epoll_wait……

在这个过程中,有两种工作模式:

1.1 ET模式

ET模式即Edge Triggered工作模式。

如果我们在第1步将rfd添加到epoll描述符的时候使用了EPOLLET标志(epoll_ctl函数参2),那么在第5步调用epoll_wait之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。

1) 基于非阻塞文件句柄

  • 就比实例一中父进程中read改为readn要求一次读500个字节,父进程pfd[0]设置的是ET模式,子进程向pfd[1]管道写了498个字节,父进程epoll_wait触发,readn只读取498未满500,就一直阻塞等待,而没法重新进入下一轮循环触发epoll_wait(如果子进程在父进程读取498个数据的时候及时又补上了2个字节那就不会导致readn一直在那阻塞,又或者把传入readn的pfd[0]通过fcntl改为非阻塞,即使未到达readn要求的字节数也不会阻塞)

2) 只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

缓冲区剩余未处理完的数据不会导致epoll_wait返回。(新的事件满足才会被触发)

struct epoll_event event;

event.events = EPOLLIN|EPOLLET;

1.2 LT模式

LT模式即Level Triggered工作模式。

与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。

缓冲区剩余未处理完的数据会导致epoll_wait返回

比较

LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。(fcntl可以设置fd的阻塞或非阻塞)在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

ET(edge-triggered):ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).

1.3 实例一

基于管道epoll ET触发模式

#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <errno.h>
#include <unistd.h>

#define MAXLINE 10

int main(int argc, char* argv[])
{
    int efd, i;
    int pfd[2];
    pid_t pid;
    char buf[MAXLINE], ch = 'a';

    pipe(pfd);
    pid = fork();
    if (pid == 0) {   // 子写
        close(pfd[0]);   // 子进程这本关闭读端
        while (1) {
            //aaaa\n
            for (i = 0; i < MAXLINE / 2; i++)
                buf[i] = ch;
            buf[i - 1] = '\n';
            ch++;
            //bbbb\n
            for (; i < MAXLINE; i++)
                buf[i] = ch;
            buf[i - 1] = '\n';
            ch++;
            //aaaa\nbbbb\n
            write(pfd[1], buf, sizeof(buf));
            sleep(2);
        }
        close(pfd[1]);
    }
    else if (pid > 0) {   //父进程 读
        struct epoll_event event;
        struct epoll_event resevent[10];
        int res, len;
        close(pfd[1]);

        efd = epoll_create(10);
        /* event.events = EPOLLIN; */
        event.events = EPOLLIN | EPOLLET;  //读事件 | ET边缘触发事件        
        /* ET 边沿触发 ,默认是水平触发 */
        event.data.fd = pfd[0];
        epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);  //管道读端的文件描述符添加进efd树中

        while (1) {
            res = epoll_wait(efd, resevent, 10, -1);  //阻塞等待事件的触发并返回事件触发的已经就绪的文件描述符
            printf("res %d\n", res);  //打印是否为pfd[0]
            if (resevent[0].data.fd == pfd[0]) {
                len = read(pfd[0], buf, MAXLINE / 2);  
                //只读取一半出来 -->管道中还剩下bbbb\n
                //由于对pfd[0]设置的是ET触发,因此进入下一轮while循环时,如果子进程没写东西进管道,epoll_wait是不会触发的
                //也就是ET模式下缓冲区残留的数据是不会触发epoll_wait
                //如果子进程又写入cccc\n,那么epoll_wait就会被触发,但此时处理的却是上一次残留在缓冲区的bbbb\n
                write(STDOUT_FILENO, buf, len);
            }
        }
        readn的情况下:
        /*
        int flag = fcntl(pfd[0],F_GETEL);   //获取pfd[0]的信息属性
        flag |= O_NONBLOCK;  //设置为非阻塞
        fcntl(connfd,F_SETFL,flag);  //修改pfd[0]属性
         while (1) {
            res = epoll_wait(efd, resevent, 10, -1);  //阻塞等待事件的触发并返回事件触发的已经就绪的文件描述符
            printf("res %d\n", res);  //打印是否为pfd[0]
            if (resevent[0].data.fd == pfd[0]) {

                len = readn(pfd[0], buf, 500);  //要求一次读取500字节,肯定不够,必会阻塞
                //父进程中read改为readn要求一次读500个字节,父进程pfd[0]设置的是ET模式,
                //子进程向pfd[1]管道写了498个字节,父进程epoll_wait触发,readn只读取498未满500,
                //就一直阻塞等待,而没法重新进入下一轮循环触发epoll_wait(如果子进程在父进程读取498个数据的时候
                //及时又补上了2个字节那就不会导致readn一直在那阻塞,又或者把传入readn的pfd[0]通过fcntl改为非阻塞,
                //即使未到达readn要求的字节数也不会阻塞)
                //因此要把pfd[0]设置为非阻塞  

                write(STDOUT_FILENO, buf, len);
            }
        }
        */                                   
        close(pfd[0]);
        close(efd);
    }
    else {
        perror("fork");
        exit(-1);
    }
    return 0;
}

如果不重新产生设置fd时的某个事件,上一次处理完数据后缓冲区中残留的数据在下一轮循环中不会被epoll_wait触发,fd不会被传出进行处理

如果触发了设置fd的某个事件,就会去处理上次fd残留在缓冲区的数据,而这次触发的fd的某个事件产生在缓冲区的数据,如果不设置好数据互通的处理方式,就只能留到等下次触发了fd事件时才能被处理

1.4 实例二

基于网络C/S模型的epoll ET触发模式

server:

/* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>

#define MAXLINE 10
#define SERV_PORT 8080

int main(void)
{
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int listenfd, connfd;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    int i, efd;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

    listen(listenfd, 20);

    struct epoll_event event;
    struct epoll_event resevent[10];
    int res, len;
    efd = epoll_create(10);
    event.events = EPOLLIN | EPOLLET;  /* ET 边沿触发 ,默认是水平触发 */

    printf("Accepting connections ...\n");
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len);
    printf("received from %s at PORT %d\n",
           inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
           ntohs(cliaddr.sin_port));
    //设置句柄为非阻塞
    int flag = fcntl(connfd,F_GETEL);   //获取connfd的信息属性
    flag |= O_NONBLOCK;  //设置为非阻塞
    fcntl(connfd,F_SETFL,flag);  //修改connfd属性

    event.data.fd = connfd;
    epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);

    while (1) {  //只监听一个connfd
        res = epoll_wait(efd, resevent, 10, -1);
        printf("res %d\n", res);
        if (resevent[0].data.fd == connfd) {
            len = read(connfd, buf, MAXLINE / 2);
            write(STDOUT_FILENO, buf, len);
        }     
    }
    return 0;
}

client: 

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>

#define MAXLINE 10
#define SERV_PORT 8080

int main(int argc, char* argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, i;
    char ch = 'a';

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

    while (1) {
        for (i = 0; i < MAXLINE / 2; i++)
            buf[i] = ch;
        buf[i - 1] = '\n';
        ch++;

        for (; i < MAXLINE; i++)
            buf[i] = ch;
        buf[i - 1] = '\n';
        ch++;

        write(sockfd, buf, sizeof(buf));
        sleep(10);
    }
    Close(sockfd);
    return 0;
}

1.5 实例三

基于网络C/S非阻塞模型的epoll ET触发模式

server:

/* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#define MAXLINE 10
#define SERV_PORT 8080

int main(void)
{
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int listenfd, connfd;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    int i, efd;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

    listen(listenfd, 20);

    struct epoll_event event;
    struct epoll_event resevent[10];
    int res, len;
    efd = epoll_create(10);
    event.events = EPOLLIN | EPOLLET;  /* ET 边沿触发 ,默认是水平触发 */

    printf("Accepting connections ...\n");
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len);
    printf("received from %s at PORT %d\n",
           inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
           ntohs(cliaddr.sin_port));
    //设置句柄为非阻塞
    int flag = fcntl(connfd,F_GETEL);   //获取connfd的信息属性
    flag |= O_NONBLOCK;  //设置为非阻塞
    fcntl(connfd,F_SETFL,flag);  //修改connfd属性

    event.data.fd = connfd;
    epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);

    while (1) {  //只监听一个connfd
        res = epoll_wait(efd, resevent, 10, -1);
        printf("res %d\n", res);
        if (resevent[0].data.fd == connfd) {
            len = read(connfd, buf, MAXLINE / 2);
            write(STDOUT_FILENO, buf, len);
        }     
    }
    return 0;
}

client:

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>

#define MAXLINE 10
#define SERV_PORT 8080

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, i;
    char ch = 'a';
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    while (1) {
        for (i = 0; i < MAXLINE/2; i++)
            buf[i] = ch;
        buf[i-1] = '\n';
        ch++;

        for (; i < MAXLINE; i++)
            buf[i] = ch;
        buf[i-1] = '\n';
        ch++;

        write(sockfd, buf, sizeof(buf));
        sleep(10);
    }
    Close(sockfd);
    return 0;
}

2. epoll反应堆模型

epoll ET模式 + 非阻塞 + void *ptr

2.1 void *ptr

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):
参四结构体的成员data的成员变量

void* ptr在一定程度可以代替int fd(共用体):

struct evt{
   int fd;
   void* (func)(int a);
}* ptr;
void *ptr设置为struct evt*类型的结构体指针,包含了fd已经回调函数func,而这个回调函数又称为epoll的反应堆

反应堆既要监听读事件也要监听写事件的,当读事件触发时,读取完后想写给客户端,但在网络中是不能随便写的,需要判断条件是否充足,比如滑动窗口win缓冲区大小是否足够写等,因此需要让void *ptr设置回调函数去处理写事件

2.2 实例 -- recv

#include<stdio.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#include<time.h>
#define MAX_EVENTS 1024  //监听上限
#define BIFLEN 4096
#define SERV_PORT 8080  //端口号

void recvdata(int fd, int events, void *arg);   
void senddata(int fd, int events, void *arg);

/*描述就绪文件描述符相关信息*/
struct myevent_s{
    int fd;  //要监听的文件描述符
    int events;  //对应的监听事件
    void *arg;  //泛型参数 --struct myevent_s *,arg指向整个结构体的首地址,也就是存着strcut myevent_s
    void(*call_back)(int fd, int events, void *arg);   //回调函数 -- 反应堆,lfd和cfd不同
    //lfd的回调函数是acceptconn -- 用处理客户端的请求链接
    //cfd的回调函数recvdata和sendata,用于处理读事件和写事件 -- recvdata读取完后会修改回调函数为sendata用于写数据到缓冲区
    int status;  //是否在监听:1 --在红黑树上(监听); 0 --不在(不监听)
    char buf[BBUFLEN]; //缓冲区
    int len;  
    long last_active;  //记录每次加入红黑树 g_efd 的事件值
};  //void *ptr,文件描述符的信息以及反应堆call_back回调函数

int g_efd;  //全局变量,保存epoll_create返回的文件描述舒服
struct myevent_s g_events[MAX_EVENTS+1];  //自定义结构体类型数组,+1 --> listen fd
//最后一位存储lfd,前面的都是存储cfd的void *ptr -- struct myevent_s

/*将结构体myevent_s成员变量初始化 -- 参1,是数组g_events的成员*/
void evenset(struct myevent_s *ev, int fd, void(*call_back)(int,int,void*), void *arg);  //arg和ev实际上是一回事
{  //把监听套接字lfd放在了g_events数组的最后一位
    ev->fd = fd;
    ev->call_bakc = call_back;
    ev->events = 0;
    ev->arg = arg; //泛型参数,arg指向ev自己的整体,其结构体数据存在g_events数组当中
    ev->status = 0;  //是否监听
    meset(ev->buf, 0, sizeof(ev->buf));  //清空缓冲区,最好是添加个判断
    ev->len = 0;  
    ev-last_active = time(NULL);  //调用even函数事件
    return;
}  //ptr中的内容 -- 反应堆的初始化

/*对myevent_s数组中新到来的epoll_data_t data中的cfd设置新的挂点
    添加到epoll监听的红黑树,是不是新到来的通过status判断*/
//evenradd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
void eventadd(int efd, int events, struct myevent_s *ev)
{
    //设置树的挂点:epoll_data_t data和events事件
    struct epoll_event epv = {0, {0}};
    int op;
    epv.data.ptr = ev;  //void *ptr:ev和ptr指向的相同的空间,给挂点的data中的ptr赋值上 
    epv.events = ev->events = events;  //EPOLLIN 或者 EPOLLOUT
    //ev和ptr指向相同的空间,
    if(ev->status == 0){  //查看是否在红黑树g_efd上,0表示不在
        op = EPOLL_CTL_ADD;  //将其加入红黑树,并且将status置为1    
        ev->status = 1;
    }
    if(epoll_ctl(efd,op,ev->fd,&epv)< 0)   //实际添加/修改
        printf("event add failed [fd=%d],events[%d]\n", ev->fd, events);
    else
        printf("event add ok [fd=%d],op=%d, events[%X]\n", ev->fd, op, events);
    return ;

}  //创建节点,将反应堆模型赋值到epoll_event节点上(data.ptr),然后再将节点挂到树上

/*创建 socket,初始化lfd,放在可g_events数组的最后一位 */
void initlistensocket(int efd, short port)
{
    struct sockaddr_in sin;
    int lfd=Socket(AF_INET,SOCK STREAM, 0);
    fcntL(lfd,F_SETFL,O_NONBLOCK); //将socket设为非阻塞

    memset(&sin,0,sizeof(sin));  //bzero(&sin, sizeof(sin))
    sin.sin_family = AF_INET;
    sin.sin_addr.s addr = INADDR_ANY;
    sin.sin_port = htons(port);

    bind(lfd,(struct sockaddr *)&sin, sizeof(sin));
    listen(lfd,20);

    /* 33行 void eventset(struct myevent_s *ev, int fd, void (*call back)(int, int, void *), void *arg); */
    eventset(&g_events[MAX_EVENTS],lfd, acceptconn, &g_events[MAX_EVENTS]);
    //初始化lfd的epoll_data_t data中的void *ptr的成员变量,回调函数是acceptconn
    //g_events在定义时候MAX_EVENTS+1了,因此这里的参1其实就是数组最后一位的地址
    //acception是lfd的回调函数(反应堆),用于处理客户端的请求连接和初始化cfd的epoll_data_t data中的void *ptr的成员变量以及添加到红黑树

    /*void eventadd(int efd, int events, struct myevent s *ev)*/
    eventadd(efd,EPOLLIN,&g_events[IMAX EVENTS]);
    //将客户端挂到g_efd红黑树上

    return;
}

/*从epoll 监听的 红黑树中删除一个 文件描述符*/
void eventdel(int efd, struct myevent_s *ev){   //ev指向g_events数组对应的元素
    struct epoll_event epv ={0,{0}};
    if(ev->status != 1)  //不在红黑树上
        return ;
    epv.data.ptr = ev;  //ev的地址赋值给了epv.data.ptr
    //epv.data.ptr = NULL;
    //对ptr指向的内容进行清空,也就是将该cfd在g_events数组中对应的元素进行清除。这里省略
    //也可以不用,因为下面对元素的status赋值为0,其它为清除的等有新的客户端前来也会被覆盖掉
    ev->status =0;  //修改状态

    epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);//从红黑树 efd 上将 ev->fd 摘除

    return ;
}

/*当有文件描述符就绪,epoll返回,调用该函数 与客户端建立链接*/
//有客户端请求连接,在main函数中epoll_wait传出的数组events包含lfd,通过调用回调函数进行处理
void acceptconn(int lfd,int events, void *arg){
    struct sockaddr in cin;
    socklen tlen =sizeof(cin);
    int cfd, i;

    if((cfd=accept(lfd,(struct sockaddr *)&cin,&len))== -1){
        if(errnO !=EAGAIN && errno != EINTR){
            /*暂时不做出错处理 */
        }
        printf("%s:accept,%s\n",__func__,strerror(errno));  //__func__当前函数的名字
        return;
    }
    do {
        for(i=0;i<MAX EVENTS; i++)  //从全局数组g_events中找一个空闲元素
            if(gevents[i].status ==0)  //类似于select中设计的client[]数组中找值为-1的元素
                break;  //跳出 for

    if(i ==MAX EVENTS){
        printf("%s: max connect limit[%d]\n",__funC__,MAX_EVENTS);
        break;   //跳出do while(0)不执行后续代码
    }

    int flag = 0;
    if((flag =fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0 ){  //将cfd也设置为非阴寒
        printf("%s:fcntl nonblocking failed, %s\n",__func__,strerror(errno));
        break;
    }

    /*给cfd设置一个 myevent_s 结构体,回调函数 设置为 recvdata */
    eventset(&g_events[i],cfd,recvdata,&g_events[i]);  //参四让arg指向了g_event[i],再赋值给ptr中的arg指向g_events[i],相当于指向了自己ptr
    eventadd(g_efd,EPOLLIN,&g_events[i]);  //将cfd添加到红黑树g_efd中,监听读事件

    } while(0);
    printf("new connect[%s:%d][time:%ld], pos[%d]\n"inet_ntoa(cin.sin addr),ntohs(cin.sin port),g events[i].last active, i);
    return;
}

//有读事件 -> 读取完后 -> cfd从g_efd上摘除下来:eventdel -> 更改回调函数为sendata
//-> 改为写事件 -> 重新挂到红黑树上 -> sendata将新的数据写给客户端
void recvdata(int fd, int events, void *arg){
    struct myevent_s *ev =(struct myevent_s *)arg;  //指向g_events数组当中的对应的数据的
    int len;

    //读文件描述符,数据存入myevent_s成员buff中
    len =recv(fd,ev->buf, sizeof(ev->buf),0);
    //代替read用于网络通信当中,第四个参数1传0表示实际读,传MSG_PEEK则是模拟读

    eventdel(g_efd, ev);//将该节点从红黑树上摘除,并对应在g_events中的元素的stauts状态改为0,表示未监听
    if(len >0){
        ev->len = len;
        ev->buf[len]='\0';  //手动添加字符串结束标记
        printf("c[%d]:%s\n",fd,ev->buf);
        eventset(ev,fd,senddata,ev)//设置该 cfd 对应的回调函数为 senddata
        eventadd(g_efd, EPOLLOUT, ev);  //将cfd加入红黑树g_efd中,监听其写事件

        }else if(len ==0){
            close(ev->fd);
            /*ev-g_events 地址相减得到偏移元素位置 */
            printf("[fd=%d] pos[%ld],closed\n",fd, ev-g_events);
         }else {
             close(ev->fd);
             printf("recv[fd=%d] error[%d]:%s\n", 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); //从红黑树g_efd中移除

    if(len >0){
        printf("send[fd=%d],[%d]%s\n",fd, len, ev->buf);
        eventset(ev,fd,recvdata,ev); //将该fd的 回调函数改为 recvdata
        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[])
    unsigned short port = SERV PORT;
    if(argc ==2)
        port = atoi(argv[1]);    //使用用户指定端口.如未指定,用默认端口
    g_efd = epoll_create(MAX EVENTS+1);  //创建红黑树,返回给全局 g_efd
    if(g_efd <=0)
        printf("create efd in %s err %s\n",_func_,strerror(errno));

    initlistensocket(g_efd, port); //初始化监听socket

    struct epoll_event events[MAX EVENTS+1];  //保存已经满足就绪事件的文件描述符数组
    printf("server running:port[%d]\n",port)

    int checkbos =0,i;
    while(1){
    /*超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */

        long now = time(NULL);  //当前时间
        for(i=0;i<100;i+,checkpos++){  //一次循环检测100个。使用checkpos控制检测对象
            if(checkpoS == MAX EVENTS)
                checkpos =0;
            if(g_events[checkpos].status != 1)  //不在红黑树 g_efd 上
            continue;
        //客户端在一定时间内不活跃就将其踢掉
        long duration =nowg_events[checkpos].last_active;  //客户端不活跃的世间
        if(duration>=60){
             close(g_events[checkpos].fd);//关闭与该客户端链接
             pfintf("[fd=%d] timeout\n",g_events[checkpos].fd);
             eventdel(g_efd, &g_events[checkpos]);//将该客户端 从红黑树 g_efd移除
        }
        /*监听红黑树g_efd,将满足的事件的文件描述符加至events数组中,1秒没有事件满足,返回 0*/
        int nfd = epoll_wait(g_efd,events, MAX EVENTS+1,1000);  //1000是毫秒单位 -- 1秒
        //客户端的请求连接处理省略,添加if语句判断event中有没有lfd然后调用回调函数即可
        //调用回调函数acceptconn(int lfd,int events, void *arg):
        //arg传:(强转)(event[0].data.ptr)->arg,将自己传给回调函数
        //这么传其实和传g_events[最大] -- lfd放在了最后一位没啥区别,不过如果是调用cfd的回调函数就不太方便,因为在g_events数组中的位置不好确定
        //events[i]知道了客户端,通过其data.ptr->arg就可以知道在g_events中的数据,传给回调函数就能对挂点进行处理和删除更改为写监听

        if(nfd < 0){
            printf("epoll_wait error, exit\n");
            break;
        }
        for(i=0; i< nfd; i++){
        /*使用自定义结构体myevent_s类型指针,接收 联合体data的void *ptr成员*/
            struct myevent_s *ev =(struct myevent_s*)events[i].data.ptr;  //注意强转,prt指向全局结构体g_events[i]
            //epoll_wait传出数组events[i] fd的事件要和fd的*ev结构体中的事件相同
            if((events[i].events & EPOLLIN)&&(ev->events & EPOLLIN))  //读就绪事件
                ev->call_back(ev->fd,events[i].events,ev->arg);  //调用*ptr中的回调函数
                //调用从cfd对应的myevents_s结构体g_events[i] == ev中的的回调函数,且将ev->arg自己传给ev自己的成员回调函数,让回调函数对ev进行操作 -- 自己给了成员
                //其实参4ev->arg和传events[i].data.ptr没啥区别,都是g_events[i],只不过ptr比arg更高一级(ptr->arg),而arg和call_back都是成员
                //成员变量arg传给成员回调函数call_back做参数不会显得太突兀,实际上还是将g_events[i]传给了call_back
                //其最终目的都是为了处理该cfd的读请求,然后将其从g_efd上去掉和将status更改为0,再改为写监听添加到g_efd
                //Lfd EPOLLIN
            //读完后将cfd的回调改为写回调sendata和将事件改为写事件,在下一次epoll_wait监听到将数据写给客户端
            if((events[i].events & EPOLLOUT)&&(ev->events & EPOLLOUT))  //写就绪事件
                ev->call_back(ev->fd,events[i].events,ev->arg);
        }
    }
    //退出前释放所有资源
    return 0;
}

2.3 ptr成员和g_events的关系

eventset是设置好void* ptr -- struct myevent_s中的相关内容((struct epoll_event).data.ptr),eventadd是创建struct epoll_event节点,将eventset创建的反应堆赋值到节点上

了解代码前要先搞清除挂点的data和events成员,data中的ptr和fd成员,ptr中的结构体成员 -- arg、call_back等,arg和ptr实际上是一回事

为什么传arg给回调函数而不直接传ptr呢?可以看代码251行后的注释

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/886251.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++基类构造器的自动调用

C基类构造器的自动调用 虽然基类的构造器和解构器不会被派生类继承&#xff0c;但它们会被派生类的构造器和解构器自动调用&#xff0c;今天我们用代码实证一下。 验证代码 源代码&#xff0c;仔细看注释内容&#xff1a; D:\YcjWork\CppTour>vim c2004.cpp #include &l…

Ubuntu下安装Zookeeper集群

Zookeeper集群是一个开源的分布式协调服务系统&#xff0c;它由Apache软件基金会维护&#xff0c;旨在为分布式应用提供一致性和可靠性的服务。 在Zookeeper集群中&#xff0c;服务器可以扮演三种角色——领导者&#xff08;Leader&#xff09;、跟随者&#xff08;Follower&a…

如何使用ssm实现基于HTML的中国传统面食介绍网站的搭建+vue

TOC ssm758基于HTML的中国传统面食介绍网站的搭建vue 第1章 绪论 1.1选题动因 当前的网络技术&#xff0c;软件技术等都具备成熟的理论基础&#xff0c;市场上也出现各种技术开发的软件&#xff0c;这些软件都被用于各个领域&#xff0c;包括生活和工作的领域。随着电脑和笔…

Python和C++混淆矩阵地理学医学物理学视觉语言模型和算法模型评估工具

&#x1f3af;要点 优化损失函数评估指标海岸线检测算法评估遥感视觉表征和文本增强乳腺癌预测模型算法液体中闪烁光和切伦科夫光分离多标签分类任务性能评估有向无环图、多路径标记和非强制叶节点预测二元分类评估特征归因可信性评估马修斯相关系数对比其他准确度 Python桑…

数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall

数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall 数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall 数据量&#xff1a;3k 想要进一步了解&#xff0c;请联系。 DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&a…

Python Tips6 基于数据库和钉钉机器人的通知

说明 起因是我第一版quant程序的短信通知失效了。最初认为短信是比较即时且比较醒目的通知方式&#xff0c;现在看来完全不行。 列举三个主要问题&#xff1a; 1 延时。在早先还能收到消息的时候&#xff0c;迟滞就很严重&#xff0c;几分钟都算短的。2 完全丢失。我手机没有…

Mac 电脑配置yolov8运行环境实现目标追踪、计数、画出轨迹、多线程

&#x1f947; 版权: 本文由【墨理学AI】原创首发、各位读者大大、敬请查阅、感谢三连 &#x1f389; 声明: 作为全网 AI 领域 干货最多的博主之一&#xff0c;❤️ 不负光阴不负卿 ❤️ 文章目录 &#x1f4d9; Mac 电脑 配置 yolov8 环境&#x1f4d9; 代码运行推理测试模型训…

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-09-27

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-09-27 目录 文章目录 计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-09-27目录1. VisScience: An Extensive Benchmark for Evaluating K12 Educational Multi-modal Scientific Reasoning VisScience:…

kubeadm部署k8s集群,版本1.23.6;并设置calico网络BGP模式通信,版本v3.25--未完待续

1.集群环境创建 三台虚拟机&#xff0c;一台master节点&#xff0c;两台node节点 (根据官网我们知道k8s 1.24版本之后就需要额外地安装cri-dockerd作为桥接才能使用Docker Egine。经过尝试1.24后的版本麻烦事很多&#xff0c;所以此处我们选择1.23.6版本) 虚拟机环境创建参考…

Webstorm 中对 Node.js 后端项目进行断点调试

首先&#xff0c;肯定需要有一个启动服务器的命令脚本。 然后&#xff0c;写一个 debug 的配置&#xff1a; 然后&#xff0c;debug 模式 启动项目和 启动调试服务&#xff1a; 最后&#xff0c;发送请求&#xff0c;即可调试&#xff1a; 这几个关键按钮含义&#xff1a; 重启…

Geoserver关于忘记密码的解决方法

第一次安装后&#xff0c;如果你设置密码那一栏一直都是默认的话&#xff0c;那么登录密码应该是账户 admin&#xff0c;密码 geoserver 但是&#xff0c;如果你自己设置了密码和账户&#xff0c;登录又登录不上&#xff0c;或者忘记了&#xff0c;有以下方法可以解决。 本质…

CSS——文字闪烁效果

CSS——文字闪烁效果 今天来完成一个文字闪烁的动态效果&#xff0c;具体呈现效果如下&#xff1a; 文字闪烁动态效果 实现步骤 基础的样式 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"vi…

收单外包机构备案分析及建议

2020年9月16日&#xff0c;中国支付清算协会&#xff08;下称“中支协”或“协会”&#xff09;公示了首批收单外包服务机构备案名单。历经5年&#xff0c;约进行50次公示后&#xff0c;截至9月21日共备案收单外包机构32457家&#xff0c;取消备案机构316家&#xff0c;拟取消机…

8642 快速排序

### 思路 快速排序是一种分治算法&#xff0c;通过选择一个基准元素将数组分成两部分&#xff0c;然后递归地对每部分进行排序。每次分区后输出当前排序结果。 ### 伪代码 1. 读取输入的待排序关键字个数n。 2. 读取n个待排序关键字并存储在数组中。 3. 对数组进行快速排序&am…

【路径规划】基于球向量的粒子群优化(SPSO)算法在无人机路径规划中的实现

摘要 本文介绍了基于球形矢量的粒子群优化&#xff08;Spherical Particle Swarm Optimization, SPSO&#xff09;算法&#xff0c;用于无人机&#xff08;UAV&#xff09;路径规划。SPSO算法通过引入球形矢量的概念&#xff0c;增强了粒子群在多维空间中的探索和利用能力&…

安全中心 (SOC) 与 网络运营中心 (NOC)

NOC 和 SOC 之间的区别 网络运营中心 (NOC) 负责维护公司计算机系统的技术基础设施&#xff0c;而安全运营中心 (SOC) 则负责保护组织免受网络威胁。 NOC 专注于防止自然灾害、停电和互联网中断等自然原因造成的网络干扰&#xff0c;而 SOC 则从事监控、管理和保护。 NOC 提…

Junit和枚举ENUM

断言机制&#xff0c;JAVA中的断言机制是一种用于检查程序中某个条件是否为真的机制。它可以在程序运行时检查某个条件是否满足&#xff0c;如果不满足则会抛出AssertionError异常。 在java中,断言机制默认是关闭的。所以会输出u。 断言机制只是为了用来吃调试程序的&#xff0…

Electron 安装以及搭建一个工程

安装Node.js 在使用Electron进行开发之前&#xff0c;需要安装 Node.js。 官方建议使用最新的LTS版本。 检查 Node.js 是否正确安装&#xff1a; # 查看node版本 node -v # 查看npm版本 npm -v注意 开发者需要在开发环境安装 Node.js 才能编写 Electron 项目&#xff0c;但是…

C++中stack和queue的模拟实现

目录 1.容器适配器 1.1什么是适配器 1.2STL标准库中stack和queue的底层结构 1.3deque的简单介绍 1.3.1deque的原理介绍 1.3.2deque的优点和缺陷 1.3.3deque和vector进行排序的性能对比 1.4为什么选择deque作为stack和queue的底层默认容器 2.stack的介绍和模拟…

MybatisPlus代码生成器的使用

在使用MybatisPlus以后&#xff0c;基础的Mapper、Service、PO代码相对固定&#xff0c;重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO、Mapper、Service等相关代码。只不过代码生成器同样要编码使用&#xff0c;也很麻烦。 这里推荐大家使…