select、poll和epoll形象化理解与对比

一、引入问题

现在有一个服务器程序和一个客户端程序,客户端程序连接上服务器程序后向服务器发送“Hello server,I am client!”,服务器收到客户端的连接后给客户端发送“Hello client,I am server!”。如果是迭代服务器,那么它运行时一旦与一个客户端程序建立连接,只有处理完这个客户端的请求(上述程序中是给客户端发完“Hello client,I am server!”)后才能与下一个客户端建立连接。也就是说,迭代服务器一次只能处理一个客户端的请求。

与迭代服务器不同的是,并发服务器一次能处理多个客户端请求。在上述程序中是服务器可以在非常短的时间内读取到多个客户端给它发送的“Hello server,I am client!”,并给多个客户端发送“Hello client,I am server!”。

我们可以用多进程或多线程的方式来实现并发服务器。父进程或父线程负责接听客户端的连接,然后交给子进程或子线程来处理该客户端的请求,而父进程继续接听客户端连接。但是在非常多的客户端请求需要服务器处理的情况下,多进程和多线程并发服务器非常消耗资源,于是又引入了多路复用来解决这个问题。

二、形象化解释select、poll、epoll

多路复用有三种方式select、poll、epoll,下面用一个生活中的例子来形象化解释这三种多路复用的原理。

假如你是便利店老板,需要看顾客是否进店了,要给顾客结账,还要防止偷窃。这时你只能一直看着门等待顾客进店,然后一直监视顾客的行为,直到顾客结账出店,你又开始等待下一个顾客。

select相当于一个监控,会替你一直监视进来的每一个顾客(select可以替你监视1024个人),顾客进店、发生偷窃行为或者顾客要结账时它都会通知你有顾客发生了事件,但是它不会通知你是哪个顾客发生了哪种事件,在没接到select的通知期间,你就可以摸鱼做其他的事,接到select的通知后,你需要一个个地问select是不是XXX人发生了XXX事件,问到以后再处理事件。

poll也是一个监控,它的功能和select相同,但是性能更好,它替你监控的人数没有限制,但是当你接到poll的通知时仍然需要一个个地问poll是不是XXX人发生了XXX事件,问到以后再处理事件。

epoll也是一个监控,它的功能更强,它不仅监视的人数没有限制,而且一旦有人进店、结账或偷窃它就会告诉你是哪个人发生了哪种事件,你就可以直接去处理事件。

三、select、poll、epoll“监控”步骤

无论你是想要select、poll还是epoll帮你监视顾客,让你可以及时知道店里现在发生了什么事然后去处理,你都需要做三步:

  1. 一旦有顾客进店,你就要告诉它们要监视这个顾客,要不然它们可不会主动帮你监视
  2. 你要准备好接收发生事件的通知
  3. 问它们具体发生了什么事件

接下来就是你去处理对应的事件了,注意:select和poll只会通知你有事件发生,想知道是谁发生了什么事件你还得一个个地问,epoll会主动告诉你是谁发生了什么事件。
换句话说,select和poll会给你一份所有监视顾客的名单,你得对着这个名单问select或poll是不是XXX人发生了XXX事件,然后去处理对应事件;epoll也会给你一份顾客名单,不过这份顾客名单上全部是发生事件的顾客,你拿到名单以后直接按着名单去处理相应事件。

四、图解select、poll、epoll多路复用实现并发服务器

select、poll、epoll用它们的”监控“功能可以实现多路复用(只一个服务器程序就可以在极短时间内处理多个客户端请求)优化服务器性能,实现服务器高并发。那它们又是如何应用到服务器上的呢?我们用并发服务器的流程图和select、poll、epoll实现的迭代服务器流程图对比来说明这个问题。
在这里插入图片描述

并发服务器流程图

五、select、poll、epoll使用的头文件和函数

为了方便理解,这里我们仍用“便利店老板看店”的例子来解释select、poll、epoll相关函数的功能

1)select

#include <sys/select.h>
#include <sys/time.h>

FD_ZERO(fd_set* fds) //清空监视顾客的集合
FD_SET(int fd, fd_set* fds) //让select监视指定的顾客
FD_ISSET(int fd, fd_set* fds) //用这个函数询问是不是XXX顾客发生了XXX事件
FD_CLR(int fd, fd_set* fds) //用这个函数可以让select不再监视指定顾客
    
int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);//select的通知由这个函数发送

2)poll

#include <poll.h>

struct pollfd
{
int fd;//相当于顾客的身份证,用于标识顾客
short events;//你关心的事件(进店、结账、偷窃)
short revents;//poll监视到实际发生的事件
} ;

int poll(struct pollfd *fds, nfds_t nfds, int timeout);//poll的通知由这个函数发送

3)epoll

#include <sys/epoll.h>

struct epoll_event
{
uint32_t events; //你关心的事件(进店、结账、偷窃)
epoll_data_t data; //相当于顾客的身份证,用于标识顾客
};

int epoll_create(int size);//创建一个epoll对象来监视顾客
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);//用这个函数可以让epoll监视指定顾客,修改你关心的事件以及让epoll不再监视某个顾客
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);//epoll的通知由这个函数发送

六、用epoll写一个简单的并发服务器程序

现在我们用epoll多路复用来实现我们最开始提到的服务器程序:客户端程序连接上服务器程序后向服务器发送“Hello server,I am client!”,服务器收到客户端的消息后给客户端发送“Hello client,I am server!”

服务器端:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/epoll.h>

#define SERVER_PORT 9999
#define MAX_EVENTS  100//epoll返回发生事件的客户端的最大数量


int main(char argc, char *argv[])
{
    int                        serv_fd;
    int                        clie_fd;
    struct sockaddr_in        serv_addr;
    struct sockaddr_in        clie_addr;
    socklen_t                cliaddr_len = sizeof(struct sockaddr_in);
    int                        rv = -1;
    char                       buf[1024];
    int                        on = 1;
    int                        epollfd;//epoll对象,用来监视所有服务器指定的客户端
    struct epoll_event         event;//用来设置单个客户端的信息,包括fd的值和对这个客户端关心的事件
    struct epoll_event         event_array[MAX_EVENTS];//用来存放所有发生事件的客户端的信息
    int                        events;//发生事件的客户端的数目
    int                        i;//用来遍历event_array数组处理对应事件
    
    

    /*服务器的初始化*/
    serv_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(serv_fd < 0)
    {
        printf("accept socket failure: %s\n", strerror(errno));
        rv = -2;
        goto cleanup;
    }
    printf("create socket[%d] successfully!\n",serv_fd);
    
    setsockopt(serv_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
   
    serv_addr.sin_family = AF_INET; 
    serv_addr.sin_port = htons(SERVER_PORT);   
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 
    if(bind(serv_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
    {
        printf("bind socket[%d] failure: %s\n", serv_fd, strerror(errno));
        rv = -3;
        goto cleanup;
    }
    
    listen(serv_fd, 13);

    /*创建epoll对象,来监视所有服务器指定的客户端*/
    epollfd = epoll_create(MAX_EVENTS);
    if(epollfd < 0)
    {
        printf("epoll_create() failure: %s\n", strerror(errno));
        rv = -4;
        goto cleanup;
    }

    /*将服务器用于和客户端建立连接的socket文件描述符加入epoll对象的监视中,如果它发生事件说明有新的客户端正在请求建立连接*/
    event.data.fd = serv_fd;
    event.events = EPOLLIN;//设置对serv_fd关心的事件为是否可读
    if(epoll_ctl(epollfd, EPOLL_CTL_ADD, serv_fd, &event) < 0)//将服务器用于和客户端建立连接的socket文件描述符加入epoll对象的监视中
    {
        printf("epoll add listen socket failure: %s\n", strerror(errno));
        rv = -5;
        goto cleanup;
    }
   
    while(1)
    {
        printf("\nStart waiting epoll inform...\n");
        events = epoll_wait(epollfd, event_array, MAX_EVENTS, -1);//接收epoll监视通知,如果监视的socket文件描述符有事件发生,函数就会返回,否则一直堵塞
        if(events < 0)
        {
            printf("epoll failure: %s\n", strerror(errno));
            break;
        }
        else if(events == 0)
        {
            printf("epoll get timeout\n");
            continue;
        }
        
        for(i=0; i<events; i++)
        {
            if((event_array[i].events&EPOLLERR) || (event_array[i].events&EPOLLHUP))
            {
                printf("epoll_wait get error on fd[%d]: %s\n", event_array[i].data.fd, strerror(errno));
                epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
                close(event_array[i].data.fd);
            }

            if(event_array[i].data.fd == serv_fd)//有新客户端连接
            {
                clie_fd = accept(serv_fd, (struct sockaddr*)NULL, NULL);
                if(clie_fd < 0)
                {
                    printf("accept new client failure: %s\n", strerror(errno));
                    continue;
                }

                event.data.fd = clie_fd;
                event.events = EPOLLIN;
                if(epoll_ctl(epollfd, EPOLL_CTL_ADD, clie_fd, &event) < 0)
                {
                    printf("epoll add client socket failure: %s\n", strerror(errno));
                    close(event_array[i].data.fd);
                    continue;
                }
            }
            else
            {//处理已经建立连接的客户端的事件
                rv = read(event_array[i].data.fd, buf, sizeof(buf));
                if(rv <= 0)
                {
                    printf("socket[%d] read failure or get disconncet and will be removed.\n",event_array[i].data.fd);
                    epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);//从epoll对象的监视中移除
                    close(event_array[i].data.fd);
                    continue;
                }
                else
                {
                    printf("socket[%d] read get %d bytes data:%s\n", event_array[i].data.fd, rv, buf);

                    if(write(event_array[i].data.fd, "Hello client, I am cloud server!", sizeof("Hello client, I am cloud server!")) < 0)
                    {
                        printf("socket[%d] write failure: %s\n", event_array[i].data.fd, strerror(errno));
                        epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
                        close(event_array[i].data.fd);
                    }
                }
            }
        }
    }

cleanup:
    if(serv_fd > 0)
        close(serv_fd);
    if(clie_fd > 0)
        close(clie_fd);
    return rv;
}

客户端:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <getopt.h>

#define SERVER_PORT         9999
#define SERVER_IP           "127.0.0.1"
#define MSG_STR				"Hello server, I am client!"


int main(char argc, char *argv[])
{
    int 					conn_fd = -1;
    int 					rv = 0;
    char					buf[1024];
    struct sockaddr_in 		serv_addr;
    
    conn_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (conn_fd < 0)
    {
        printf("create socket failure: %s\n", strerror(errno));
        rv = -2;
        goto cleanup;
    }
    
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);
    inet_aton( SERVER_IP, &serv_addr.sin_addr );

    if( connect(conn_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
    {
        printf("connect to server[%s:%d] failure: %s\n", serv_ip, port, strerror(errno));
        rv = -3;
        goto cleanup;
    }

    rv = write(conn_fd, MSG_STR, sizeof(MSG_STR));
    if(rv < 0)
    {
        printf("Write data to server [%s:%d] failure: %s\n", serv_ip, port, strerror(errno));
        rv = -4;
        goto cleanup;
    }

    memset(buf, 0, sizeof(buf));
    rv = read(conn_fd, buf, sizeof(buf));
    if(rv < 0)
    {
        printf("Read data to server [%s:%d] failure: %s\n", serv_ip, port, strerror(errno));
        rv = -5;
        goto cleanup;
    }
    printf("Read %d bytes data from Server: %s\n", rv, buf);


cleanup:
    if(conn_fd > 0)
        close(conn_fd);

    return rv;

}

七、对比总结select、poll、epoll

首先说明,select和poll的用处越来越有限,epoll已经成为了目前实现高性能网络服务器的必备技术。

select的缺点:

  1. 最多只能监视1024个文件描述符,虽然可以更改,但select采用轮询的方式扫描文件描述符,文件描述符数量越多,select性能越差。
  2. 内核/用户空间内存拷贝问题,会产生巨大的开销。
  3. 需要遍历整个文件描述符数组才能发现哪些文件描述符发生了那些事件。
  4. 如果程序没有完成对一个就绪文件描述符事件的处理,那么每次调用select都还是会将那些正在处理的文件描述符通知给进程。

而poll除了没有最大文件描述符限制,以上的缺点都存在。

epoll与select、poll的实现机制完全不同,它没有以上的缺点,可以直接返回就绪文件描述符数组,能显著提高程序在大量并发连接中只有少量活跃情况下的系统CPU利用率。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值