目录
一、概念
二、使用
1.select系统调用
代码实现
前言:
一般多客户端在和服务器通信时,服务器在执行recv时会先阻塞,然后按照顺序依次处理客户端,无论客户端有无数据都会被处理,这样大大降低了执行效率。此时就引入i/o复用技术,提高网络程序效率
io的处理方式是没有数据的客户端忽略不管,一旦接收就阻塞起来,有数据的客户端接收。
一、概念
- I/O复用可以同时监听多个文件描述符/多个套接字,以及检测套接字描述符内有无数据
 - 通常,以下情况下需要使用i/o复用: 
  
- 客户端程序要同时处理多个socket,虽然多线程也可以解决该问题,但是当客户端个数逐渐增大时,在多线程之间切换的开销会大大加大。
 - 客户端程序要同时处理用户输入和网络连接
 - TCP服务器要同时处理监听socket和连接socket。这是i/o复用使用最多的场合
 - 服务器要同时处理TCP请求和UDP请求
 - 服务器要同时监听多个端口,或处理多种服务
 
 - Linux下实现i/o复用的系统调用主要有select、poll和epoll。
 -  
i/o复用操作流程:
-  
先检查哪些描述符上有数据,哪些无数据
 -  
找到有数据的描述符,然后进行处理
 
 -  
 
需要指出的是,I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依处理其中的每一个文件描述符,这使得服务器看起来好像是串行工作的。如果要提高并发处理的能力,可以配合使用多线程或多进程等编程方法。

二、使用
1.select系统调用
- 用途:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。
 - 函数原型:
 
#include<sys/select.h>
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout); 
- 参数: 
  
- 第一个参数nfds指定被监听的文件描述符总数。通常被设置为select监听的所有文件描述符中的最大值+1,因为文件描述符是从0开始计数的。
 - 第二、三、四个参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通常从这三个参数中传入自己感兴趣的文件描述符。
 - 第五个参数timeout用来设置select函数的超时事件。它是timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。 
    
-  
struct timeval { long tv_sec;//微秒 long tv_usec;//微秒数 }select提供了一个微秒级的定时方式。如果timeout变量的tv_sec成员和tv_usec成员都传递0,则select将立即返回。如果timeout传递NULL,则select将一直阻塞,直到某个文件描述符就绪。
 
 -  
 - 返回值:select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。这三个参数时fd_set结构指针类型。fd_set结构体定义在下面讲。
 
 - 返回值: 
  
- 成功时返回就绪(可读、可写和异常)文件描述符的总数(不会告知是哪个文件描述符,而是总数)。
 - 返回值为0表示超时,在超时时间内没有任何文件描述符就绪
 - 返回值为-1可能是失败,并设置srrno;如果在在select等待期间,程序收到信号,则select也会立即返回-1,并设置为EINTR。
 
 

fd_set结构体定义:

由以上定义可见,该结构体仅包含了一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符由FD_SETSIZE指定,可容纳文件描述符的位数是1024位,0-1023。按位偏移,0放在第一个位上。
由于位操作的繁琐(按位与规则:1&1=1. 1&0=0. 0&1=0. 0&0=0. 只要和1相与结果不为0则为真,也就是该文件描述符被设置过。1>>3 就是第三位和1按位与操作),我们使用下面的一系列宏来访问fd_set结构体中的位:
#include<sys/select.h>
FD_ZERO(fd_set *fdset); //轻触fdset中所有位
FD_SET(int fd,fd_set *fdset);//设置fdset的位fd
FD_CLR(int fd,fd_set *fdset);//清除fdset的位fd
int FD_ISSET(int fd,fd_set *fdset);//测试fdset的位fd是否被设置 
 什么叫事件就绪?
 以读事件和写事件为例:
- 读事件:当执行recv()时,把数据发送到接收缓冲区,如果接收缓冲区满,此时recv就会被阻塞,读事件就没有就绪,反之,缓冲区未满,读事件就绪
 - 写事件:当执行send()时,把数据写入到发送缓冲区,如果发送缓冲区满,此时send就会被阻塞,写事件就没有就绪,反之,缓冲区未满,写事件就绪
 
文件描述符就绪条件:
哪些情况下文件描述符可以被认为是可读、可写或异常情况,对于select的使用非常的关键。
- 在网络编程中以下情况socket可读: 
  
- socket内核接收缓冲区中字节数大于或等于其低水位标记SO_RECVLOWAT。此时可以无阻塞的读该socket,并且读操作返回的字节数大于0.
 - socket通信的对方关闭连接。此时对该socket的读操作返回0
 - 监听socket上有新的连接请求
 - socket上有未处理的错误。此时可以使用getsockopt来读取和清除该错误。
 
 - 以下socket可写: 
  
- socket内核发送缓冲区中的可用字节数>=其低水位标记SO_SNDLOWAT。此时我们可以无阻塞的写该socket,并且写操作返回的字节数>0
 - soket的写操作被关闭。对写操作被关闭的socket执行写操作将出发一个SIGPOPE信号。
 - socket使用非阻塞connect连接成功或失败(超时)之后
 - socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误
 
 - 网络编程中,select能处理的异常只要一种:socket上接收到带外数据
 
代码实现
i/o实现流程:
1.i/o函数检测描述符的数据有无情况。返回值会告知有几个
2.具体检测哪个文件描述符有数据
2.多个描述符内查找我们感兴趣的事件
下面用代码实现在键盘输入文件描述符,有数据打印出来,没数据就进行检测,最长等待5秒:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/select.h>
#include<sys/time.h>
#define STDIN 0
int main()
{
	int fd=STDIN;
	fd_set fdset;//收集描述符
//1.描述符中最大值+1
	while(1)//如果有多个描述符就要循环存放
	{
		FD_ZERO(&fdset);//清空集合
		FD_SET(fd,&fdset);//把键盘对应的描述符fdset添加到集合内
		struct timeval tv={5,0};//超时时间
		int n=select(fd+1,&fdset,NULL,NULL,&tv);//当前操作写事件
		if(n==-1)
		{
			printf("select err\n");
		}
		else if(n==0)
		{
			printf("time out\n");
		}
		else//就绪状态,有用户输入
		{
			if(FD_ISSET(fd,&fdset))//fd_isset()判断当前描述符内是否有数据,有数据则返回为真,执行下面语句
			{
				char buff[128]={0};
				read(fd,buff,127);//开始读取数据
				printf("buff=%s\n",buff);//输出数据
			}
		}
	}
}
 
运行结果:

如果超过5秒未输入或者输入的时间超过5秒还未发送,都会弹出“time out”超时的提示
如果上次的还没发出去(输入的太慢超过5秒)会在缓冲区内存着跟着下次一起发送
用select实现tcp服务器代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/select.h>
#include<sys/time.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define MAXFD 10
//初始化描述符
void fds_init(int fds[])
{
	for(int i=0;i<MAXFD;i++)
	{
		fds[i]=-1;
	}
}
//添加描述符
void fds_add(int fd,int fds[])
{
	for(int i=0;i<MAXFD;i++)
	{
		if(fds[i]=-1)//说明未被使用
		{
			fds[i]=fd;
			break;
		}
	}
}
//移除描述符
void fds_del(int fd,int fds[])
{
	for(int i=0;i<MAXFD;i++)
	{
		if(fds[i]==fd)
		{
			fds[i]=-1;
			break;
		}
	}
}
//创建tcp监听套接字
int socket_init()
{
	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(sockfd==-1)
		return -1;
	struct sockaddr_in saddr;
	memset(&saddr,0,sizeof(saddr));
	saddr.sin_family=AF_INET;
	saddr.sin_port=htons(6000);
	saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
	int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
	if(res==-1)
		return -1;
	res=listen(sockfd,5);
	if(res==-1)
		return -1;
	return sockfd;
}
int accept_client(int sockfd)
{
	struct sockaddr_in caddr;
	int len=sizeof(caddr);
	int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
	return c;
}
int main()
{
	int fds[MAXFD];//用于收集描述符
	fds_init(fds);//初始化描述符
	int sockfd=socket_init();//初始化套接字
	if(sockfd==-1)
		exit(0);
	fds_add(sockfd,fds);//将套接字添加到描述符
	fd_set fdset;//集合收集描述法
	//找到描述符最大值,并将描述法都添加到集合内
	while(1)
	{
		FD_ZERO(&fdset);
		int maxfd=-1;
		for(int i=0;i<MAXFD;i++)
		{
			if(fds[i]==-1)
				continue;
			FD_SET(fds[i],&fdset);//将数组中有效(>=0)的描述符添加到集合
			if(fds[i]>maxfd)
			{
				maxfd=fds[i];//找到最大的描述符
			}
		}
		struct timeval tv={5,0};
		int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//读
		if(n==-1)
		{
			printf("select err\n");
		}
		else if(n==0)
		{
			printf("time out\n");
		}
		else
		{
			for(int i=0;i<MAXFD;i++)
			{
				if(fds[i]==-1)
				{
					continue;
				}
				if(FD_ISSET(fds[i],&fdset))//测试该监听套接字是否有数据
				{
					if(fds[i]==sockfd)//监听套接字,accept
					{
						int c=accept_client(fds[i]);
						if(c!=-1)
						fds_add(c,fds);//添加新接收的连接
					}
				
					else//连接套接字,recv处理
					{
						char buff[128]={0};
						int num=recv(fds[i],buff,127,0);
						if(num<=0)
						{
							close(fds[i]);
							fds_del(fds[i],fds);
						}
						else
						{
							printf("recv:%s\n",buff);
							send(fds[i],"ok",2,0);
						}
					}
				}
			}
		}
	}
}
                


















