alin的学习之路(Linux网络编程:五)(epoll ET\LT模式、epoll反应堆模型)

alin的学习之路(Linux网络编程:五)(epoll ET\LT模式、epoll反应堆模型)

1. epoll ET/LT模式

ET:边沿触发

LT(默认):水平触发(持续触发)

1. ET模式

  • 边沿触发。
    • 缓冲区 剩余未读尽的数据, 不会导致 epoll_wait 返回。 新的事件满足,会触发epoll_wait
      返回。
    • Epoll 的 ET模式 是高效模式。 但是 ,只支持 非阻塞 O_NONBLOCK 方式 。 —— 轮询。
  • 设置方法:
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event)
 
int flg = fcntl(cfd, F_GETFL);
flg |= O_NONBLOCK;  // 添加 非阻塞属性
fcntl(cfd, F_SETFL, flg);

2. LT模式

  • 水平触发。—— 默认采用的模式
    • 缓冲区 剩余未读尽的数据, 会导致 epoll_wait 返回。

3. 修改 fd 非阻塞

使用 fcntl() 函数

int fcntl(int fd, int cmd, ... /* arg */ );
	fd:文件描述符
	cmd:命令
		F_GETFL: 获取当前文件属性。
		F_SETFL:  设置当前文件属性。
	...: 变参, 根据命令 传递不同参数。
        
// 修改非阻塞
int flg = fcntl(fd,F_GETFL);
flg |= O_NONBLOCK;      // 添加 非阻塞属性
fcntl(fd,F_SETFL,flg);

2. epoll 优缺点

  • 优点:
    • 增加ET模式,通信高效。
    • 突破 1024 限制。
    • 实现简单。
  • 缺点:
    • 不能跨平台。 Linux

3. epoll反应堆模型分析

1. 初版epoll思路分析

socket()、Bind()、Listen() —> epoll_create() 创建监听红黑数 —> 返回 epfd —> epoll_ctl() —> EPOLL_CTL_ADD —> 向树上添加一个监听 fd —> while (1) —> epoll_wait 监听 —> 对应监听fd有事件产生 —> 返回满足监听条件的数组。 —> 判断 返回的元素 —> lfd 满足 —> Accept() —> cfd 满足 —> read() —> 小 – 大 —> write() 写回去。

2. epoll反应堆模型思路分析

-----核心思想:不仅要监听cfd的读事件,还要监听cfd的写事件

socket()、Bind()、Listen() —> epoll_create() 创建监听红黑数 —> 返回 epfd —> epoll_ctl() —> EPOLL_CTL_ADD —> 向树上添加一个监听 fd —> while (1) —> epoll_wait 监听 —> 对应监听fd有事件产生 —> 返回满足监听条件的数组。 —> 判断 返回的元素 —> lfd 满足 —> Accept() —> cfd 满足 —> read() —> 小 – 大 —> epoll_ctl() 将cfd 从树上摘下 —> 设置EPOLLOUT 和 写的回调函数 —> 使用epoll_ctl() 向树上添加 cfd 的 写事件 EPOLL_CTL_ADD —> 调用 epoll_wait() —> 返回说明cfd可写 —> write()写回去 —> 将cfd从树上摘下 —> 设置EPOLLIN 并 设置读回调函数 —> 使用epoll_ctl() 向树上添加 cfd 的 写事件 EPOLL_CTL_ADD —> 调用 epoll_wait() 。。。

3. 源码分析

1. 关键结构体

struct myevent_s {
  	int fd;                         //要监听的文件描述符
  	int events;                       //对应的监听事件
  	void *arg;                        //泛型参数
  	void (*call_back)(int fd, int events, void *arg);    //回调函数
  	int status;        			//是否在监听:1->在红黑树上(监听), 0->不在(不监听)
  	char buf[BUFLEN];
  	int len;
  	long last_active;     		//记录每次加入红黑树 g_efd 的时间值
};

2. main 函数

  1. 设置端口号,可从命令行获取,默认8080

  2. epoll_create() 创建红黑树,将返回的红黑树根存在全局变量 g_efd 中

  3. initlistensocket() 函数初始化 lfd ,相当于Socket()、Bind()、Listen()

  4. while(1){

    1. 超时验证,如果超时则断开连接,从树上摘下
    2. 调用 epoll_wait() 监听满足条件的 lfd 或 cfd
      1. 如果是读事件,启动读回调
      2. 如果是写事件,启动写回调
  5. }

3. initlistensocket 函数

  1. socket() 创建监听套接字 lfd
  2. 设置 lfd 非阻塞 ,使用 fcntl() 函数
  3. bind() 绑定ip 和 端口号
  4. listen() 设置最大监听个数
  5. eventset() 函数初始化 lfd 的结构体,自定义的全局数组中将 lfd 放在最后一个位置,即下标为 MAX_EVENTS 的位置
  6. eventadd() 函数添加读事件,将 lfd 挂上红黑树

4. eventset 函数

  1. 初始化自定义结构体
  2. 设置回调函数。 —— 不同场景下, 可以借助 eventset 函数的调用,设置不同回调。
    lfd —> acceptconn()
    cfd ----> recvdata()
    cfd ----> senddata()

5. eventadd 函数

  1. 传入事件 EPOLLIN
  2. 在函数中设置 epoll_ctl() 函数的选项
  3. 将 cfd或lfd 挂上红黑树

6. acceptconn 函数

  1. 调用 accept() 函数,创建通信套接字 cfd
  2. 遍历 g_events[] 数组找到第一个 status 为 0 的位置将该 cfd 对应的结构体添加进去
  3. 设置 cfd 非阻塞
  4. 调用 eventset函数, 初始化,设置 读回调 —— recvdata()
  5. 调用 eventadd函数,设置 EPOLLIN 监听 读事件。

7. recvdata 函数

  1. 使用 recv() 函数读数据

  2. eventdel() 从树上摘除

  3. 如果 recv() 的返回值

    1. 大于0 :处理字符串,设置该 fd 的回调函数为 senddata() ,重新设置结构体 eventset() ,然后 eventadd() 挂到红黑树上,设置 EPOLLOUT 监听 写事件。

    2. 等于0 :关闭文件描述符 close(fd)

    3. 小于0 :关闭文件描述符 close(fd)

8. senddata 函数

  1. 使用 send() 函数读数据
  2. eventdel() 从树上摘除
  3. 如果 send() 的返回值
    1. 大于0 :处理字符串,设置该 fd 的回调函数为 recvdata() ,重新设置结构体 eventset() ,然后 eventadd() 挂到红黑树上,设置 EPOLLIN 监听 读事件。
    2. 其他 :关闭文件描述符 close(fd)

4. epoll 反应堆模型源代码

/*
 *epoll基于非阻塞I/O事件驱动
 */
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#define MAX_EVENTS  1024                                    //监听上限数
#define BUFLEN 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;                                              //泛型参数
    void (*call_back)(int fd, int events, void *arg);       //回调函数
    int status;                                             //是否在监听:1->在红黑树上(监听), 0->不在(不监听)
    char buf[BUFLEN];
    int len;
    long last_active;                                       //记录每次加入红黑树 g_efd 的时间值
};

int g_efd;                                                  //全局变量, 保存epoll_create返回的文件描述符
struct myevent_s g_events[MAX_EVENTS+1];                    //自定义结构体类型数组. +1-->listen fd


/*将结构体 myevent_s 成员变量 初始化*/

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));
    //ev->len = 0;
    ev->last_active = time(NULL);                       //调用eventset函数的时间

    return;
}

/* 向 epoll监听的红黑树 添加一个 文件描述符 */

//eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
void eventadd(int efd, int events, struct myevent_s *ev)
{
    struct epoll_event epv = {0, {0}};
    int op;
    epv.data.ptr = ev;
    epv.events = ev->events = events;       //EPOLLIN 或 EPOLLOUT

    if (ev->status == 0) {                                          //不在红黑树 g_efd 上 
        op = EPOLL_CTL_ADD;                 //将其加入红黑树 g_efd, 并将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[%0X]\n", ev->fd, op, events);

    return ;
}

/* 从epoll 监听的 红黑树中删除一个 文件描述符*/

void eventdel(int efd, struct myevent_s *ev)
{
    struct epoll_event epv = {0, {0}};

    if (ev->status != 1)                                        //不在红黑树上
        return ;

    //epv.data.ptr = ev;
    epv.data.ptr = NULL;
    ev->status = 0;                                             //修改状态
    epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);                //从红黑树 efd 上将 ev->fd 摘除

    return ;
}

/*  当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */

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) {
        if (errno != EAGAIN && errno != EINTR) {
            /* 暂时不做出错处理 */
        }
        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)                                //类似于select中找值为-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]);   
        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 ;
}

void recvdata(int fd, int events, void *arg)
{
    struct myevent_s *ev = (struct myevent_s *)arg;
    int len;

    len = recv(fd, ev->buf, sizeof(ev->buf), 0);            //读文件描述符, 数据存入myevent_s成员buf中

    eventdel(g_efd, ev);        //将该节点从红黑树上摘除

    if (len > 0) {

        ev->len = len;
        ev->buf[len] = '\0';                                //手动添加字符串结束标记
        printf("C[%d]:%s\n", fd, ev->buf);

        eventset(ev, fd, senddata, ev);                     //设置该 fd 对应的回调函数为 senddata
        eventadd(g_efd, EPOLLOUT, ev);                      //将fd加入红黑树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 ;
}

/*创建 socket, 初始化lfd */

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);

    /* 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]);

    /* void eventadd(int efd, int events, struct myevent_s *ev) */
    eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);

    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 checkpos = 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 = now - g_events[checkpos].last_active;       //客户端不活跃的世间

            if (duration >= 60) {
                close(g_events[checkpos].fd);                           //关闭与该客户端链接
                printf("[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);
        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;  

            if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {           //读就绪事件
                ev->call_back(ev->fd, events[i].events, ev->arg);
                //lfd  EPOLLIN  
            }
            if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {         //写就绪事件
                ev->call_back(ev->fd, events[i].events, ev->arg);
            }
        }
    }

    /* 退出前释放所有资源 */
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_41775886/article/details/107794901