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

一、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:定时阻塞监控时间
  1、null   永久等下去
  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 64
const 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的优越之处:因为后者每次调用时都要传递你所要监控的所有socket给select/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几乎没有上限(与内存有关)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值