Linux网络编程(三)多路IO转接服务器

来源:互联网 发布:nginx 静态页面 编辑:程序博客网 时间:2024/06/06 00:32

一、select
采用的是集合的方式,将关心的事件放置集合队列(最多监听1024个)中,轮询访问(每次都会检测所有的句柄)拿到一个已就绪的就会返回,(内核态到用户态的切换来拿事件),内部再使用位运算,将可读,可写,异常三个事件分开来,
1、select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯的改变进程打开的文件描述符个数,并不能改变select监听的文件个数。
2、解决了1024以下客户端使用select 是很合适的,select采用的是轮询模式,但如果连接客户过多,会大大降低服务器的响应效率。
3、循环次数过多,每次有一个文件描述符准备好就会返回,可能就会一直处于激活状态,因为有文件描述符的拷贝(每次都要扫描注册的文件描述符集合,将已准备好的文件描述符返回给用户),系统从内核态切换用户态的次数会过多,造成性能下降

int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);//nfds: 监控的文件描述符的个数//exceptfds 监控异常发生到达文件描述符集合//timeout:定时阻塞监控时间  1null   永久等下去  2、设置timeval  ,等待固定时间  3、将timeval中时间设置为0,检查描述字后立即返回,轮询

二、poll
poll 比select 能 好一点,也是在指定时间内轮询一定数量的文件描述符,以测试是否有文件描述符就绪

三、epoll
把用户关心的文件描述符直接放置内核里的一个事件表(红黑树)中,不会像select与poll那样,每次调用都要传入文件描述符集或事件集,epoll_wait()函数当检测到就绪事件时,会将已准备好的文件描述符拷贝到它第二个参数指向的数组(链表)中,系统只需要从数组中取事件即可。用户态与内核态切换的次数并不多,极大地提高了效率

三个系统调用int epoll_create(int size);  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);  首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。epoll_ctl可以操作上面建立的epoll,例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。
#include <stdio.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <string.h>#include <stdlib.h>#include <sys/epoll.h>#include <unistd.h>#define  SIZE 64const char* msg = "HTTP/1.0 200 OK\r\n\r\n<html><h1>hello epoll!<h1></html>\r\n";static void Usage(const char* proc){    printf("Usage: \n\t %s [local_ip][local_port]\n\n",proc);}int startup(const char *ip,int port){    int sock = socket(AF_INET,SOCK_STREAM,0);//创建套结字    if(sock<0)    {        perror("socket");        exit(2);    }    //当服务器异常关闭时,清除套结字,就可再次重启    int opt = 1;    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));    //设置结构体,填充自己ip与端口号    struct sockaddr_in local;    local.sin_family = AF_INET;    local.sin_port = htons(port);//端口号的转换    local.sin_addr.s_addr = inet_addr(ip);//ip    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)//绑定套结字    {        perror("bind");        exit(3);    }    if(listen(sock,10)<0)//监听队列,里面有10个文件描述符,返回已经准备好链接的那一个    {        perror("listen");        exit(4);    }    return sock;}int main(int argc,char* argv[]){        if(argc!=3)        {            Usage(argv[0]);            return 1;        }        int listen_sock = startup(argv[1],atoi(argv[2]));//把得到的套结字作为监听套结字        int epfd = epoll_create(256);//创建epoll模型        if(epfd < 0)        {            perror("epoll_create   is   failed\n");            return 5;        }        printf("listen_sock:%d\n",listen_sock);        //设置结构体事件,并把事件与套结字放在就绪队列中        struct epoll_event ev;        ev.events = EPOLLIN;//表示对应的文件描述符可以读        ev.data.fd = listen_sock;        epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);//事件注册函数,将监听套结字加入监听事件        int nums = -1;        int timeout = 10000;//可设置-1阻塞0非阻塞          //struct timeval timeout = {1,0};   //设置为0,0  非阻塞状态        struct epoll_event revs[SIZE];    //####################################################################################################        //对已连接的客户端进行数据处理        while(1)        {            switch((nums = epoll_wait(epfd,revs,SIZE,timeout)))//监听并判断是否有文件描述符属性发生改变        //后三个参数为输出形,等文件描述符就位,返回0   -1    或着已就位的文件描述符的个数            {                case 0: printf("timeout...\n");break;                case -1:perror("epoll");break;                default:                {                    int i = 0;                    for(i=0;i<nums;i++)                    {                        int fd = revs[i].data.fd;//从就绪队列拿出已就绪好的fd                        if(fd == listen_sock && (revs[i].events &EPOLLIN))//检测监听套结字是否存在链接                        {                            //listen socket ready!                            struct sockaddr_in client;//                            socklen_t len = sizeof(client);                            int rw_sock=accept(listen_sock,(struct sockaddr*)&client,&len) ;//客户端向服务器发出链接请求                            //服务器利用accept()来接受请求,建立连接,并拿到客户端套结字                            if(rw_sock <0)                            {                                perror("accept failed");                                continue;                            }                            printf("get a new client :[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));//输出客户端ip与端口号                            ev.events = EPOLLIN;//设置文件描述符为可读                            ev.data.fd = rw_sock;//监听此套结字                            epoll_ctl(epfd,EPOLL_CTL_ADD,rw_sock,&ev);//将拿到的套结字加入监听事件                        }                        else if(fd != listen_sock) //                        {                                if(revs[i].events & EPOLLIN)//有数据来临时,(接收客户端数据)                                {                                    //read ready                                    char buf[1024];                                    ssize_t s = read(fd,buf,sizeof(buf)-1);                                    if(s>0) // read success                                    {                                        buf[s] = 0;                                        printf("client#  %s\n",buf);//输出读的数据                                        ev.events = EPOLLOUT;//重置事件为有数据要写                                        ev.data.fd = fd;                                        epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);                                    }                                    else if(s ==0)//没有读到数据                                    {                                        printf("client  is  quit!\n");                                        close(fd);//关闭文件描述符                                        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);//删除                                    }                                    else                                    {                                        perror("read");                                        close(fd);                                        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);                                    }                                }                                else if(revs[i].events & EPOLLOUT)//有数据要写时                                {                                    //write                                    write(fd,msg,strlen(msg));//写msg内容                                    close(fd);//关闭并删除                                    epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);                                }                        }                    }                }            }        }    return 0;    }
从上面的调用方式就可以看到epoll比select/poll的优越之处:因为后者每次调用时都要传递你所要监控的所有socketselect/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。而我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。所以,实际上在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

epoll______LT(电平触发) 默认的,相当于高效的poll

在事件就绪时,epoll_wait()会通知你,你可以不对此事件作出反应。当下次再调用epoll_wait()时,当你没有对此事作出反应时,还用通知你

ET(边缘触发)epoll的高效工作模式,(需向epoll内核事件表中注册一fd上的EPOLLET事件)

当事件就绪,epoll_wait()通知你,你必须处理此事件,因为下次不会再通知你

三种I/O复用区别:
从原理上看:
select 与poll都是采用了轮询方式去访问文件描述符集,每次返回准备就绪的一个,时间复杂度为O(n),epoll_wait()采用回调方式,当检测到有就绪描述符时,会触发回调函数,将就绪描述符加载到就绪队列中,内核会在合适的时机将事件拷贝到用户空间,时间复杂度为O(1)
从内存消耗:
select/poll将所监控的文件描述符从用户态copy到内核态,切换频率高,效率低下
文件描述符上限
epoll几乎没有上限(与内存有关)

阅读全文
1 0
原创粉丝点击