c++webserver项目--select,poll,epoll详解

select,poll,epoll详解

小白一枚,欢迎大家批评指正哈!


前言

linux网络变编程主要通过select,poll,epoll三种IO多路复用技术,即可以利用select,poll,epoll同时监控多个文件描述符,提高cpu执行效率。即为了避免这里cpu的空转,我们不让这个线程亲自去fd中是否发生变化,而是引进了select,poll,epoll三种IO多路复用技术,它可以同时观察许多流的I/O事件,如果没有事件,代理就阻塞,线程就不会挨个挨个去轮询了,提高了效率。


一、select详解

select是最早出现的多路复用技术,它最多可以同时监控1024个文件描述符。当调用select这个API时,会将监视的文件描述符集合拷贝到内核,由内核顺序遍历是否有文件描述符发生变化,从而返回有变化的文件描述符的个数,但是并不知道到底是哪些文件文件描述符发生了变化,需要一个个去遍历。每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除,因此涉及到两次遍历,并且每次都会把文件描述符集合拷贝到内核,存在开销大,浪费资源的缺点。

1. API介绍

int select(int nfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,struct timeval *timeout);

参数解释:
参数1:-nfds:委托内核检测的最大文件描述符的值+1
参数2:-fd_setreadfds:委托内核检测哪些文件描述符有读的属性
参数3:-fd_set
writefds:委托内核检测哪些文件描述符有写的属性(看看写缓冲区是否已满,如果未满,则可以继续写数据)
参数4:-fd_set*exceptfds:委托内核检测哪些文件描述符发生异常
参数5:-struct timeval *timeout:是一个结构体,用于设置超时时间(NULL代表永久阻塞,0代表不阻塞,大于0代表阻塞对应的时间)
返回值解释:

  • -1:失败;
  • 0: 检测的文件描述符集合中有n个文件描述符发生了变化;

其他相关函数:

1.将文件描述符fd对应的标志位设置为0
void FD_CLR(int fd,fd_setset);
2.判断fd对应的标志位是0还是1
int FD_ISSET(int fd,fd_set
set);
3.将文件描述符fd对应的标志位设置为1
void FD_SET(int fd,fd_setset);
4.初始化文件描述符集合为0
void FD_ZERO(fd_set
set);

2. 代码编写

#include<stdio.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/select.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
    //创建监听的套接字(文件描述符)
    int fd1 = socket(AF_INET,SOCK_STREAM,0);//默认传输层使用tcp协议
    if(fd1==-1){
        perror("socket");
        exit(0);
    }
    //绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;//默认使用ipv4协议
    saddr.sin_addr.s_addr = 0;
    saddr.sin_port = htons(9999);
    int ret = bind(fd1,(struct sockaddr*)&saddr,sizeof(saddr));
    if(ret == -1){
        perror("bind");
        exit(0);
    }
    //监听
    ret = listen(fd1,128);
    if(ret == -1){
        perror("listen");
        exit(0);
    }
    //创建数组用于存放多个文件描述符
    fd_set reads,temp;
    FD_ZERO(&reads);
    FD_SET(fd1,&reads);//将监听的文件描述符设置为1
    int maxfd = fd1;//将当前只有监听的文件描述符设置为最大的文件描述符值,后续再不断更新
    while(1){
        temp = reads;//用temp来进行内核区的操作
        //调用select函数,让内核帮忙检测哪些文件描述符有数据
        int ret = select(maxfd+1,&temp,NULL,NULL,NULL);
        if(ret == -1){
            perror("select");
            exit(0);
        }else if(ret == 0){
            continue;//如果为0则代表没有检测到文件描述符有数据
        }else if(ret>0){
            //代表文件描述符对应的缓冲区的数据发生了变化
            if(FD_ISSET(fd1,&temp)){
                //如果监听的文件描述符发生了变化,则代表有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(fd1,(struct sockaddr*)&cliaddr,&len);
                FD_SET(cfd,&reads);
                maxfd = maxfd>cfd?maxfd:cfd;
            }//遍历看哪些文件描述符发生了变化
            for(int i = fd1+1; i<=maxfd; i++){
                if(FD_ISSET(i,&temp)) {
                    //说明客户端有数据发送进来了
                    char buf[1024] = {0};
                    int len  = read(i,buf,sizeof(buf));
                    if(len == -1){
                        perror("read");
                        exit(0);
                    }else if(len == 0){
                        printf("client closed\n");
                        close(i);
                        FD_CLR(i,&reads);
                    }else if(len>0){
                        printf("read buf: = %s\n",buf);
                        write(i,buf,strlen(buf)+1);
                    }

                }

            }


        }
        
        
    }
    close(fd1);
    return 0;
}

二、poll详解

poll与select不同之处在于没有文件描述符的限制,并且将需要检测的文件描述符的事件和发生的文件描述符事件分开,但是都需要从内核到用户的相互拷贝,且需要顺序遍历发生变化的文件描述符

1. API介绍

struct pollfd{
int fd;//委托内核检测的文件描述符
short events;//委托内核检测文件描述符的什么事件
short revents;//文件描述符实际发生的事件
};

 int poll(struct pollfd  *fds,nfds_t nfds,int timeout);

参数解释:
–参数1:-fds 是一个结构体数值,这是一个需要检测的文件描述符集合
–参数2:-nfds 检测的文件描述符的大小
–参数3:-timeout 超时时间
返回值:
成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;

失败时,poll() 返回 -1,并设置 errno 为下列值之一:
EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds 指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
ENOMEM:可用内存不足,无法完成请求。

2. 代码编写

#include<stdio.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/select.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdlib.h>
#include<poll.h>
int main(){
    //创建监听的套接字(文件描述符)
    int fd1 = socket(AF_INET,SOCK_STREAM,0);//默认传输层使用tcp协议
    if(fd1==-1){
        perror("socket");
        exit(0);
    }
    //绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;//默认使用ipv4协议
    saddr.sin_addr.s_addr = 0;
    saddr.sin_port = htons(9999);
    int ret = bind(fd1,(struct sockaddr*)&saddr,sizeof(saddr));
    if(ret == -1){
        perror("bind");
        exit(0);
    }
    //监听
    ret = listen(fd1,128);
    if(ret == -1){
        perror("listen");
        exit(0);
    }
    //初始化检测的文件描述符组
    struct pollfd fds[1024];
    for(int i = 0; i<1024; i++){
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = fd1;
    int nfds = 0;
    while(1){
        int ret = poll(fds,nfds+1,-1);//-1代表如果文件描述符不发生改变则阻塞
        if(ret == -1){
            perror("poll");
            exit(0);
        }else if(ret == 0){
            continue;//如果为0则代表没有检测到文件描述符有数据
        }else if(ret>0){
            //代表文件描述符对应的缓冲区的数据发生了变化
            if(fds[0].revents&POLLIN){
                //如果监听的文件描述符发生了变化,则代表有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(fd1,(struct sockaddr*)&cliaddr,&len);
                for(int i = 1; i<1024; i++){
                    if(fds[i].fd==-1){
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }
                nfds = nfds>cfd?nfds:cfd;
            }//遍历看哪些文件描述符发生了变化
            for(int i = 1; i<=nfds; i++){
                if(fds[i].revents&POLLIN) {
                    //说明客户端有数据发送进来了
                    char buf[1024] = {0};
                    int len  = read(fds[i].fd,buf,sizeof(buf));
                    if(len == -1){
                        perror("read");
                        exit(0);
                    }else if(len == 0){
                        printf("client closed\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    }else if(len>0){
                        printf("read buf: = %s\n",buf);
                        write(fds[i].fd,buf,strlen(buf)+1);
                    }

                }

            }


        }
        
        
    }
    close(fd1);
    return 0;
}

三、epoll详解

直接封装了三个函数实现,直接在内核中开辟一块内存,用于存放监控的文件描述符(红黑树)和已经就绪的文件描述符(双向链表),不需要顺序遍历全部的文件描述符,只需遍历已经发生变化的文件描述符

1. API介绍

int epoll_create(int size);//在内核中创建一块内存,包含两块数据,用于存放监控的文件描述符(红黑树)和已经就绪的文件描述符(双向链表),返回值为epfd,一个epoll实例
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* events)//对文件描述符进行增删改
int epoll_wait(int epollfd, struct epoll_event* events, int maxevent, int timeout);
//返回就绪的文件描述符个数,具体的发生变化的数据封装在events这个结构体中,第三个参数为指定最多监听多少个事件,其值必须大于0

2. 代码编写

#include<stdio.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/select.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/epoll.h>
int main(){
    //创建监听的套接字(文件描述符)
    int fd1 = socket(AF_INET,SOCK_STREAM,0);//默认传输层使用tcp协议
    if(fd1==-1){
        perror("socket");
        exit(0);
    }
    //绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;//默认使用ipv4协议
    saddr.sin_addr.s_addr = 0;
    saddr.sin_port = htons(9999);
    int ret = bind(fd1,(struct sockaddr*)&saddr,sizeof(saddr));
    if(ret == -1){
        perror("bind");
        exit(0);
    }
    //监听
    ret = listen(fd1,128);
    if(ret == -1){
        perror("listen");
        exit(0);
    }
    //创建一个epoll实例


    int epfd = epoll_create(100);
    //将监听的文件描述符的相关信息加入到epoll实例中
    struct epoll_event epev;//创建一个结构体
    epev.events = EPOLLIN;//检测读事件
    epev.data.fd = fd1;
    epoll_ctl(epfd,EPOLL_CTL_ADD,fd1,&epev);
    

    while(1){
        //检测哪些有数据
        struct epoll_event epevs[1024];
        int ret = epoll_wait(epfd,epevs,1024,-1);
        if(ret == -1){
            perror("epoll_wait");
            exit(0);
        }
        printf("ret : %d\n",ret);//检测到有几个发送了改变


        for(int i = 0; i<ret; i++){
            if(epevs[i].data.fd==fd1){
                //监听的文件描述符有数据到达(有客户端连接)
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(fd1,(struct sockaddr*)&cliaddr,&len);
                //将连接到的客户端的文件描述符添加到实例中去
                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);
            }else{
                //有数据到达
                char buf[1024] = {0};
                int len  = read(epevs[i].data.fd,buf,sizeof(buf));
                if(len == -1){
                    perror("read");
                    exit(0);
                }else if(len == 0){
                    printf("client closed\n");
                    epoll_ctl(epfd,EPOLL_CTL_DEL,epevs[i].data.fd,NULL);
                    close(epevs[i].data.fd);
                }else if(len>0){
                    printf("read buf: = %s\n",buf);
                    write(epevs[i].data.fd,buf,strlen(buf)+1);
                }
            }
        }


    }
    close(fd1);
    close(epfd);
    return 0;
}

总结

  • poll 和select都是轮询的方式,内核每次都扫描整个注册的文件描述符集合;而epoll_wait采用回调方式,内核检测到就绪的文件描述符时,触发回调函数,回调函数将该文件描述符上对应的事件插入内核就绪队列,最后返回给用户。
  • 当事件触发比较频繁时,回调函数也会被频繁触发,此时效率就未必比select 或 poll高了。
    所以epoll的最佳使用情景是:连接数量多,但活跃的连接数量少。其他情况下epoll的效率较高。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值