socket网络编程实现并发服务器——IO多路复用

你。 提交于 2020-02-28 22:44:27

一 五种网络I/O模型

在Linux下进行网络编程时,服务器端编程经常需要构造高性能的IO模型,常见的IO模型有五种:
(1)同步阻塞IO
(2)同步非阻塞IO(Non-blocking IO)
在这里插入图片描述
(3)IO多路复用(IO Multiplexing)
:IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数 可以避免同步非阻塞IO模型中轮询等待的问题,此外poll、epoll都是这种模型。
在这里插入图片描述
(4) 信号驱动IO(signal driven IO)
(5)异步IO(Asynchronous IO)

各服务器源代码:https://gitee.com/constructorvirgil/lingyun_apue/tree/master/yangjianing

二 多路复用–select

select()函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时 间后才唤醒它,然后接下来判断究竟是哪个文件描述符发生了事件并进行相应的处理。

#include <sys/select.h>
#include <sys/time.h>
    
 struct timeval   
 {  
       long tv_sec;   //seconds   
       long tv_usec;  //microseconds 
 };
       FD_ZERO(fd_set* fds)           //清空集合 
       FD_SET(int fd, fd_set* fds)    //将给定的描述符加入集合 
       FD_ISSET(int fd, fd_set* fds)  //判断指定描述符是否在集合中
       FD_CLR(int fd, fd_set* fds)    //将给定的描述符从文件中删除  

看一下函数原型

int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);

1 第一个参数max_fd指待测试的fd的总个数,它的值是待测试的最大文件描述符加1
2 中间三个参数readset、writeset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试的可以设置为 NULL;
3 最后一个参数是设置select的超时时间,如果设置为NULL则永不超时

下面是使用select()多路复用实现的服务器端示例代码:(只贴出主要部分)

for(i=0; i<ARRAY_SIZE(fds_array); i++)
    {
	    fds_array[i] = -1;   //说明该位置为空

    }
    fds_array[0]=listenfd;

    while (1)
    {
	    FD_ZERO(&rdset);   //清零
	    for(i=0; i< ARRAY_SIZE(fds_array);i++)  //遍历该数组
	    {
		    if (fds_array[i]<0)
	            {
			    continue;
                    }
		     maxfd = fds_array[i]>maxfd ? fds_array[i] : maxfd;        
	   	     FD_SET(fds_array[i], &rdset); 
	    }
    }


    rv = select(maxfd+1, &rdset, NULL, NULL, NULL); 
    if (rv < 0)
    {
	    printf("select failure: %s\n",strerror(errno));
	    break ;
    } 
    else if (rv == 0)
    {
	    printf("select get timeout\n");
	    continue;
    }

    if (FD_ISSET(listenfd, &rdset))  //是否是新客户端链接
    {
	    if(connfd = accept(listenfd, (struct sockaddr *)NULL,NULL) < 0)
	    {
                    printf("accept new client failure : %s\n",strerror(errno));
		    return -4;
	    }
    

    found = 0;
    for(i=0; i<ARRAY_SIZE(fds_array);i++)
    {
            if( fds_array[i] < 0)
	    {
	            printf("accept new client [%d add it into array\n",connfd);
		    fds_array[i] = connfd;
		    found = 1;
		    break ;
	    }

    }

    if( !found )   //说明客户端过多,不再进行处理
    {
	    printf("accept client [%d] full\n",connfd);
	    close(connfd);

    }

    else
    {
	    for(i=0;i<ARRAY_SIZE(fds_array); i++)
	    {
		    if( fds_array[i]<0 || !FD_ISSET(fds_array[i], &rdset) ) 
		    {
			     continue;
			      
		    }
		    if( (rv=read(fds_array[i], buf, sizeof(buf))) <= 0)
		    {
			     printf("socket[%d] read failure or get disconncet.\n", fds_array[i]); 
			     close(fds_array[i]); 
			     fds_array[i] = -1;
		    }
		    else
		    {

			     printf("socket[%d] read get %d bytes data\n", fds_array[i], rv);
			     
	            for(j<0;j<rv;j++)
			  buf[j] = toupper(buf[j]);
		    
		    if( write(fds_array[i], buf, rv) < 0 )
		    {
			    printf("socket[%d] write failure: %s\n", fds_array[i], strerror(errno)); 
			    close(fds_array[i]); 
			    fds_array[i] = -1;
		    }  
		 }
	    }
    }
}


    close(listenfd);
    return 0;
 
}
 基于select的I/O复用模型的是单进程执行可以为多个客户端服务,
 这样可以减少创建线程或进程所需要的CPU时间片或内存 资源的开销;
 此外几乎所有的平台上都支持select(),其良好跨平台支持是它的另一个优点。
 当然它也有两个主要的缺点:
 1 每次调用 select()都需要把fd集合从用户态拷贝到内核态,之后内核需要遍历所有传递进来的fd
 2单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,
 可以通过setrlimit()、修改宏定义甚至重 新编译内核等方式来提升这一限制,
 但是这样也会造成效率的降低;

三、poll多路复用

1 select()和poll()系统调用的本质一样,前者在BSD UNIX中引入的,后者在System V中引入的。poll()的机制与 select() 类 似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件 描述符数量的限制(但是数量过大后性能也是会下降).
2 poll函数的原型说明如下:

#include <poll.h>
struct pollfd 
{   int     fd;         /* 文件描述符 */  
 short   events;     /* 等待的事件 */   
 short   revents;    /* 实际发生了的事件 */ 
 } ;
 
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

第一个参数用来指向一个struct pollfd类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文 件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。
第二个参数 nfds 指定数组中监听的元素个数;
第三个参数 timeout指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll() 一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。 这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

下面是使用poll()多路复用实现的服务器端示例代码:(只贴出主要部分)

for(i=0; i<ARRAY_SIZE(fds_array); i++)
    {
	    fds_array[i].fd = -1;              //初始化poll的文件描述符

    }
    fds_array[0].fd = listenfd;               //监听的描述符
    fds_array[0].events = POLLIN;             //普通或优先级带数据可读

    max = 0;

    while (1)
    {
	    rv = poll(fds_array,max+1, -1);
	    if(rv<0)
	    {
		    printf("select failure: %s\n",strerror(errno));
		    break;
	    }
	    else if(rv == 0)
	    {
		    printf("select get timeout\n");
		    continue;
	    }



	    //监测listenfd(监听套接字)是否存在连接
	    if( fds_array[0].revents &POLLIN)
	    {
		    if((connfd = accept(listenfd,(struct sockaddr *)NULL,NULL))<0)
		    {
			    printf("accept new client failure :%s \n",strerror(errno));
			    continue;
		    }

		    found = 0;

		    //将提取到的connfd放入poll结构体数组中,以便于poll函数监测
		    for(i=0;i<ARRAY_SIZE(fds_array); i++)
		    {
                            if( fds_array[i].fd < 0 ) 
			    {
				     printf("accept new client[%d] \n", connfd );
				     fds_array[i].fd = connfd; 
				     fds_array[i].events = POLLIN;
				     found = 1; 
				     break;
			    }
		    }

                            if( !found )      //说明客户端数量过多已满
			    {
				    printf("accept new client[%d] but full ,so refuse it \n",connfd);
				    close (connfd);
			    }




		    //max更新
		    max = i>max ? i:max;

		    // 如果没有就绪的描述符,就继续poll监测,否则继续向下看
		    if(--rv<=0)
			    continue;
	    }

	    else
	    {
		     for(i=1; i<ARRAY_SIZE(fds_array); i++)           // 遍历整个数组,继续响应就绪的描述符
		     {
			      if( fds_array[i].fd < 0 )
			      {
				       continue;
			      }



                              // 接收数据
			       if( (rv=read(fds_array[i].fd, buf, sizeof(buf))) <= 0)
			       {
				        printf("socket[%d] read failure or get disconncet.\n", fds_array[i].fd); 
					close(fds_array[i].fd); 
					fds_array[i].fd = -1;
			       }
			       else
			       {
				       printf("socket[%d] read get %d bytes data\n", fds_array[i].fd, rv);
				        
				       for(j=0; j<rv; j++)
					        buf[j]=toupper(buf[j]);

				       if( write(fds_array[i].fd, buf, rv) < 0 ) 
				       {
					        printf("socket[%d] write failure: %s\n", fds_array[i].fd, strerror(errno)); 
						close(fds_array[i].fd); 
						fds_array[i].fd = -1;
				       }
			       }
		     }
	    }

    }

四、epoll多路复用

1 epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著 提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听 的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事 件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减 少epoll_wait/epoll_pwait的调用,提高应用程序效率。
2 前两者中:
select的缺点:

  1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,
  2. 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
  3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调 用还是会将这些文件描述符通知进程。
    poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在

epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统,把原先的select/poll调用分成了3 个部分:

  1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
  2. 调用epoll_ctl向epoll对象中添加这100万个连接的套接字
  3. 调用epoll_wait收集发生的事件的连接

3创建epoll实例:epoll_create()

#include <sys/epoll.h> int epoll_create(int size);

系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空。若成功返回文件描述符,若出错返回-1。 参数size指定了我们想要通过epoll实例来检查的文件描述符个数。该参数并不是一个上限,而是告诉内核应该如何为内部数据结 构划分初始大小.

修改epoll的兴趣列表:epoll_ctl()

#include <sys/epoll.h>
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
 /* 第一个参数epfd是epoll_create()的返回值; 
 第二个参数op用来指定需要执行的操作,它可以是如下几种值:
EPOLL_CTL_ADD:将描述符fd添加到epoll实例中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的 结构体中。如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl()将出现EEXIST错误; 
EPOLL_CTL_MOD:修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表 中的文件描述符,epoll_ctl()将出现ENOENT错误;
 EPOLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev。如果我们试图移除一个不在epfd的兴 趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表 移除;
 第三个参数fd指明了要修改兴趣列表中的哪一个文件描述符的设定
  第四个参数ev是指向结构体epoll_event的指针,*/

事件等待:epoll_wait()

#include <sys/epoll.h>
 int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
 /*第一个参数epfd是epoll_create()的返回值;
 第二个参数evlist所指向的结构体数组中返回的是有关就绪态文件描述符的信息,数组evlist的空间由调用者负责申请; 
 第三个参数maxevents指定所evlist数组里包含的元素个数; 
 第四个参数timeout用来确定epoll_wait()的阻塞行为:
 如果timeout等于-1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生或者直到捕获到一个信号为止。
 如果timeout等于0,执行一次非阻塞式地检查,看兴趣列表中的描述符上产生了哪个事件。 
 如果timeout大于0,调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止。
 

下面是使用epoll()多路复用实现的服务器端示例代码:

if( (epollfd=epoll_create(MAX_EVENTS)) < 0 ) 
    {
	     printf("epoll_create() failure: %s\n", strerror(errno)); 
	     return -2; 
    }
	     
    event.events = EPOLLIN; 
    event.data.fd = listenfd;

    if( epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event) < 0) 
    {

	   printf("epoll add listen socket failure: %s\n", strerror(errno));
	   return -3;
    }

    while (1)
    {
	    //程序将在此被阻塞
	    events = epoll_wait(epollfd, event_array, MAX_EVENTS, -1); 
	    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);
		    }


                    //listen socket get event表示新客户端现在开始连接
             if( event_array[i].data.fd == listenfd )
	     {
		     if( (connfd=accept(listenfd, (struct sockaddr *)NULL, NULL)) < 0) 
		     {
			     printf("accept new client failure: %s\n", strerror(errno)); 
			     continue;
		     }

		     event.data.fd = connfd;
		     event.events =  EPOLLIN;

		     if( epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event) < 0 )
		     {
			      printf("epoll add client socket failure: %s\n", strerror(errno)); 
			      close(event_array[i].data.fd); 
			      continue;
		     } 
		     printf("epoll add new client socket[%d] ok.\n", connfd);
	     }
	     else            //已连接客户端套接字获取数据
	     {
		     if( (rv=read(event_array[i].data.fd, buf, sizeof(buf))) <= 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);
			     close(event_array[i].data.fd); 
			     continue;
		     }
		     else
		     {
			     printf("socket[%d] read get %d bytes data\n", event_array[i].data.fd, rv);
			      

			     for(j=0; j<rv; j++)
		          	       buf[j]=toupper(buf[j]);


			     if( write(event_array[i].data.fd, buf, rv) < 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); 
			     }
		     }

	     }
	    }
    }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!