实现多路转接I/O——select服务器

Posted 巴山雨夜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现多路转接I/O——select服务器相关的知识,希望对你有一定的参考价值。

1、五种I/O模型

I/O就是表示的是数据的输入、输出;这就是我们之前所以理解的I/O,但是我要说的是你们之前理解的I/O只是I/O过程的一部分; I/O可以分为是两个步骤 :等待I/O事件就绪 ;    实现数据的搬迁; I/O事件是不是高效,一般与等待就绪的时间在I/O过程中所占的比重的大小有关; Unix下共有五种I/O模型:
①、阻塞I/O;
②、非阻塞I/O;
③、I/O复用(select和(e)poll);
④、信号驱动I/O(SIGIO);
⑤、异步I/O(Posix.1的aio_系列函数);

1.1、阻塞I/O模型

应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。如果数据没有准备好,一直等待。数据准备好了,从内核拷贝到用户空间,表示IO结束,IO函数返回成功指示。

1.2、 非阻塞I/O模型

我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试 数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。

1.3、 I/O复用模型

该种模型又被称作是多路转接,I/O复用模型会用到select或者poll函数,这两个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

1.4、信号驱动I/O模型

首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

1.5、 异步I/O模型

调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。也就是它只需要发起这个读写事件,不要等待与数据搬迁,只需要在结束之后得到成果。

2、几种I/O模型的比较

上面的五种I/O模型: 前面的四种模型属于是同步I/O模型都是,自己等待,自己将数据从内核拷贝到调用者的缓冲区。 而异步I/O的两个阶段都不同于前四个模型,它是只发起这个I/O操作,由内核等待,内核将数据拷贝到调用者的缓冲区,在通知它。
这五种I/O模型中最高效的是 I/O复用模型,因为它可以一次等待多个I/O事件继续; 所以我们今天主要介绍这种I/O模型。     五种I/O模型比较:
                   

3、实现文件描述符的重定向

在Linux下我们经常使用的命令 > 可以将内容重定向输出到  之后的文件中。 但是我们今天要在代码中,想使用printf函数将内容输出到一个文件中要怎么做呢? 今天我们向大家来介绍两个函数可以  将文件描述符重定向。
SYNOPSIS
       #include <unistd.h>

       int dup(int oldfd);
       int dup2(int oldfd, int newfd);
那就是dup 、dup2函数。这两个函数可以实现文件描述符的重定向。 dup函数的作用: 将oldfd文件描述符进行一份拷贝,返回值为最新拷贝生成的文件描述符(最小的未被使用的文件描述符) dup2函数的作用: 使用newfd对oldfd文件描述符做一份拷贝,必要是可以先关闭newfd文件描述符。     也就是说调用dup2之后,oldfd文件描述符不变,newfd和oldfd相等。

如何将重定向之后的文件描述符恢复?

假设我们要使用printf输出数据到文件描述符 【3】; 我需要这样写:
dup2(3,1);//我们将3号文件重定向到1号文件。1号文件为标准输出。
此时printf输出的文件就会全部输出到3号文件描述中。
但是做完之后,我们需要将文件描述符重新还原回去,要怎么做呢? 我们需要在dup2之前现将文件描述符1  使用dup函数保存一份。 之后在调用dup函数重定向回来。
代码演示:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>			  
#include<unistd.h>
#include<string.h>
//验证dup函数还有dup2函数
int main()

	//先设置文件的屏蔽为 0
	umask(0);
	int fd  = open("./log",O_CREAT|O_RDWR,0666);//创建文件  可读可写,文件权限为 666
	const  char  *  msg = "hello  world";
	printf("fd->%d\\n",fd);
	write(fd,msg,strlen(msg));
	int new_fd  = dup(fd);
	printf("new_fd->%d\\n",new_fd);
	write(new_fd,msg,strlen(msg));
	int cpfd = dup(1);//先对标准输出 文件描述符进行拷贝;
	//将fd文件描述符重定向到标准输出
	dup2(fd,1);
	printf("nice to meet you\\n");
	//再将标准输出  恢复
	dup2(cpfd,1);
	printf("nice to meet you\\n");
	return 0;
现象演示:

4、select实现多路转接

要实现select服务器我们需要了解一个函数的用法 【那就是】select函数
       /* According to POSIX.1-2001 */
       #include <sys/select.h>

       /* According to earlier standards */
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
这个函数的功能是可以实现同时等待多个文件描述符; 参数nfds表示的是等待的文件描述符中的最大的那个+1; 类型fd_set表示的是是否需要等待某个文件描述符,所以这里的fd_set底层是用位图实现的,所以我们最多可以等待的文件描述符的个数为sizeof(fd_set)*8; 参数readfds表示的是需要等待的读事件的文件描述符集; 参数writefds表示的是需要等待的写事件的文件描述符集; 参数exceptfds表示的是需要等待的异常事件的文件描述符集; 参数timeout的类型为下 ,表示的是 每次select的时间为ty_sec秒
struct timeval 
               long    tv_sec;         /* seconds */
               long    tv_usec;        /* microseconds */
           ;
另外,我们虽然知道fd_set的底层实现,但是我们不能直接对于他们进行操作,操作系统为我们提供了下面的一些接口来实现一些操作:
SYNOPSIS
       /* According to POSIX.1-2001 */
       #include <sys/select.h>

       /* According to earlier standards */
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       void FD_CLR(int fd, fd_set *set);//将文件描述符中的fd位去掉
       int  FD_ISSET(int fd, fd_set *set);//检测文件描述符集set中的fd位是否存在
       void FD_SET(int fd, fd_set *set);//为set文件描述符集设置fd为设置
       void FD_ZERO(fd_set *set);//将set文件描述符集清空
实现代码: 实现select服务器
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
#include<unistd.h>
#include<string.h>
static void usage(const  char *proc)

	printf("Usage :%s [local_ip] [local_port]\\n",proc);

//定义文件描述符数组   存储需要等待的文件描述符
int  readfds[sizeof(fd_set)*8];

//建立监听服务器
int startup(char  * ip,int port)

	//建立套接字
	int sock = socket(AF_INET,SOCK_STREAM,0);
	if(sock < 0)
	
		perror("socket");
		exit(1);
	
	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(2);
	
	//设置服务器状态为 监听状态
	if(listen(sock,5)<  0)
	
		perror("listen");
		exit(3);
	
	return sock;

int main(int argc  ,char * argv[])

	if(argc != 3)
	
		usage(argv[0]);
		return 4;
	
	int listen_sock = startup(argv[1],atoi(argv[2]));
	int  i =1 ;
	int  num = sizeof(fd_set)*8;
	readfds[0] = listen_sock;
	for(;i < num;++i)
	
		readfds[i] = -1;
	
	//定义两个文件描述符集 ,来表述要等到的文件描述符
	fd_set wfds,rfds;
	char buf[1024];
	while(1)
	
		int maxfd = -1;
		//给文件描述符集 初始化,并设置要等待的文件描述
		FD_ZERO(&rfds);
		for(i = 0;i <num;++i)
		
			if(readfds[i]!= -1)
			
				FD_SET(readfds[i],&rfds);
			
			//得到最大的文件描述符
			maxfd = maxfd < readfds[i] ?readfds[i]:maxfd;
		
		//设置成是5秒select
		struct timeval time = 1,0;
		int n = select(maxfd+1,&rfds,&wfds,NULL,&time);
		switch(n)
		
			//表示的时间结束
			case 0:
				printf("time out ..\\n");
				break;
			//表示的select失败
			case -1:
				break;
			//至少有有一个文件描述符就绪
			default:
			
				i = 0;
				//检查哪个文件描述符就绪
				for(;i < num;++i)
				
					//表示的是监听服务器就绪
					if(i ==0&&FD_ISSET(readfds[i],&rfds))
					
						struct sockaddr_in client;
						socklen_t  len = sizeof(client);
						//accep来接受客户端
						int client_sock = accept(listen_sock,(struct sockaddr*)&client,&len);	
						if(client_sock< 0 )	
						
							perror("accept");
							continue;
						
						else
						
							//将客户端的套接字放到要等待的文件描述符数组中
							int j =0;
							for(j = 1;j <num;++j)
							
								if(readfds[j] == -1)
								
									readfds[j]= client_sock;
									break;
								
							
							//要是j>num表示的是文件描述符数组已满
							if(j >= num)
							
								printf("readfds is full\\n");
								return  5;
							
						
					
					//等待的普通文件秘书符就位
					if(i != 0 &&FD_ISSET(readfds[i],&rfds))
					
						//先读数据
						int s = read(readfds[i],buf,sizeof(buf)-1);
						if(s < 0)
						
							perror("read");
							return 6;
						
						else if(s == 0)
						
							printf("client quit\\n");
							readfds[i] =-1;
							close(readfds[i]);
							continue;
						
						else
						
							buf[s]=0;
							printf("client#: %s\\n",buf);
							fflush(stdout);
							//读完之后直接将读到的数据返给客户端
							write(readfds[i],buf,strlen(buf));
						
					
				
				break;
			
		
	
	return 0;
实现客户端代码:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<unistd.h>

static void  usage()

	printf(".......:[ipaddr],[port]\\n");
	exit(1);

int main(int argc,char * argv[])

	if(argc != 3)
	
		usage();
	
	//建立套接字
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(socket < 0)
	
		perror("socket");
		return  2;
	
	struct sockaddr_in  addr;
	addr.sin_family =AF_INET;
	addr.sin_port = htons(atoi(argv[2]));
	addr.sin_addr.s_addr = inet_addr(argv[1]);
	//连接服务器
	if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0  )
	
		perror("connent");
		return 3;
	
	//成功之后 
	else
	
		printf("connect  success\\n");
 		char  buf[1024];
 		while(1)
   		
        	printf("client #:");
          	fflush(stdout);
			//客户端为服务器发数据
           	ssize_t s = read(0,buf,sizeof(buf)-1);
            if(s <=0 )
            
     			 perror("read");
       		     return 4;
    		
     		else
      		
           		buf[s] = '\\0';
				//write(sockfd,buf,strlen(buf));
				//在这使用文件描述符重定向dup函数来代替write函数为服务器写数据

				int fd = dup(1);//先保存标准输出的文件描述符
				dup2(sockfd,1);
				printf("%s",buf);
				fflush(stdout);
				dup2(fd,1);//将标准输出文件描述符恢复

         	
			//从服务器中读数据
          	s= read(sockfd,buf,sizeof(buf)-1) ;
           	if(s== 0)
            
                 printf("server quit\\n");
                 break;
            
            else if(s <0)
      			 perror("read");
        		 return 5;
     		
      		else
       		
           		 buf[s-1] = '\\0';
             	 printf("server #:%s\\n",buf);
          	
       	
		close(sockfd);
    
     return  0;
 

5、总结select服务器的优缺点

与多进程/多线程服务器进行对比 它的优点在于:
1、不需要建立多个线程、进程就可以实现一对多的通信。 2、可以同时等待多个文件描述符,效率比起多进程多线程来说要高很多;
与多进程/多线程服务器进行对比 它的缺点在于:
1、每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 ,循环次数有点多; 2、 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 。 3、 select支持的文件描述符数量太小了,默认是1024;

以上是关于实现多路转接I/O——select服务器的主要内容,如果未能解决你的问题,请参考以下文章

多路I/O转接服务器

IO多路转接 ——— selectpollepoll

I/O多路转接   ----   poll

I/O多路转接复用机制---select,poll,epoll

I/O多路转接之select和非阻塞IO

I/O多路复用之select