十分钟,深入理解epoll

136 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,[点击查看活动详情]

今天的内容笔者将基于源码,从what,why,how三个角度展开讲解epoll这个概念。

1. 什么是epoll

首先来抽象理解一下socket网络编程模型

可以将该过程理解为一个去餐厅消费的过程。

客户端:客人

服务端:消费的餐厅

listenfd:酒店门口迎宾的人,连接来了就给酒店内部的服务员(connfd)。

accept():一个行为,把客人带到大厅中,介绍给connfd服务员,后面服务员和客户之间的recv/send,都是由这个clientfd工作

    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
     
    // 创建socketfd,即这里的listenfd
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    //创建五元组 
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
     
    //绑定listenfd与当前的socket
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    
    //通过accept(),获得connfd
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    printf("========waiting for client's request========\n");
    while (1) {

        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }

再来看一下epoll的使用模型

区别于上面的一请求一线程模型,epoll能给我们带来单线程实现同时接收多请求的使用场景。

理解模型:快递员--蜂巢--住户模型

小区中有N多住户,对应一个住户所有人的大集合----就是fd集合

蜂巢对应另一个子集---今天需要寄快递的用户

epoll_create()--创建快递员

epoll_ctl()--往小区里搬进/搬出/移动位置住户

epoll_wait()--快递员多久来蜂巢取一次快递这个过程

events--快递员取快递需要拿的袋子,需要指定大小,设置的大小决定了跑几次,无非就是多跑几次,是个就绪队列,双向链表,可变的,一百万的并发,大约有一万个活跃的连接,所以不用担心数据量大了。

2. 为什么要用epoll

答案很简单,因为epoll性能高

libevent对这几个IO多路复用技术做过对比

image.png

这是一个限制了 100 个活跃连接的基准测试,每个连接发生 1000 次读写操作为止。纵轴是请求的响应时间,横轴是持有的 socket 句柄数量。

可以看出来,epoll 性能是很高的,并且随着监听的文件描述符的增加,epoll 的优势更加明显

不过,这里限制的 100 个连接很重要。epoll 在应对大量网络连接时,只有活跃连接很少的情况下才能表现的性能优异。

换句话说,epoll 在处理大量非活跃的连接时性能才会表现的优异。如果15000个 socket 都是活跃的,epoll 和 select 其实差不了太多。

3. 怎么用epoll

epoll的组成

三个耳熟能详的用户层API:

int epoll_create(int size);

函数实现功能:创建一个epoll的句柄,最新版本的内核实现中,这个size只要大于0即可。同时需要注意消耗一个fd。

返回值:返回值为一个int类型的文件描述符,作为后面两个函数的参数

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数:第一个参数是epoll_create()的返回值;第二个参数表示动作,用三个宏来表示;第三个参数是需要监听的fd;第四个参数是告诉内核需要监听什么事件;

函数实现功能:epoll的事件注册函数,epoll_ctl向 epoll对象中添加、修改或者删除感兴趣的事件

返回值:返回0表示成功,否则返回–1,此时需要根据errno错误码判断错误类型。

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

函数实现功能:等待事件的产生,类似于select()调用。

参数:参数events用来从内核得到事件的集合;maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size;参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,永久阻塞)。

返回值:该函数返回需要处理的事件数目,如返回0表示已超时。如果返回–1,则表示出现错误,需要检查 errno错误码判断错误类型

这三个API的底层实现在Linux内核,所以从使用层面来讲,只需要弄清楚这三个API的功能即可

    int epfd = epoll_create(1); //int size

    struct epoll_event events[POLL_SIZE] = {0};
    struct epoll_event ev;

    ev.events = EPOLLIN;
    ev.data.fd = listenfd;

    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

    while (1) {

            int nready = epoll_wait(epfd, events, POLL_SIZE, 5); //如果这里设置-1,表示有了再去,没有就不去
            if (nready == -1) {
                    continue;
            }

            int i = 0;
            for (i = 0;i < nready;i ++) {

                    int clientfd =  events[i].data.fd;
                    if (clientfd == listenfd) {

                            struct sockaddr_in client;
                        socklen_t len = sizeof(client);
                        if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
                            printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                            return 0;
                        }

                            printf("accept\n");
                            ev.events = EPOLLIN;
                            ev.data.fd = connfd;
                            epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);

                    } else if (events[i].events & EPOLLIN) {

                            n = recv(clientfd, buff, MAXLNE, 0);
                    if (n > 0) {
                        buff[n] = '\0';
                        printf("recv msg from client: %s\n", buff);

                                    send(clientfd, buff, n, 0);
                    } else if (n == 0) { //


                                    ev.events = EPOLLIN;
                                    ev.data.fd = clientfd;

                                    epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);

                        close(clientfd);

                    }

                    }

            }

    }


    close(listenfd);  
    return 0;
}