IO复用使得程序能够监听多个文件描述符,这对提高程序的性能至关重要。通常,网络程序在下列情况下需要使用IO复用技术。
- 客户端程序需要同时处理多个socket
- 客户端程序需要同时处理用户输入和网络连接
- tcp服务器要同时处理连接套接字和监听套接字
- 服务器需要同时处理tcp请求和udp请求
- 服务器同时要监听多个端口或者处理多种服务
select系统调用
在一段指定时间内监听用户感兴趣的文件描述符上的可读可写和异常事件
select API
#include<sys/select.h>
int select (int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,
struct timeval *timeout);
(1)nfds参数指定被监听的文件描述符的总数,它通常被设置为select监听的所有文件描述符中最大值+1,因为文件描述符是从0开始计数的。
(2)readfds,writefds,exceptfds参数分别指向可读,可写,异常事件对应的文件描述符的集合。应用程序调用select函数时,通过这三个参数传入自己感兴趣的文件描述符,select调用返回时,内核将修改他们来告诉应用程序哪些文件描述符准备就绪。
这三个参数时fd_set结构指针类型的,fd_set结构体定义如下:
fd_set
typedef struct
{
/*XPG4.2requiresthismembername.Otherwiseavoidthename
fromtheglobalnamespace.*/
#ifdef__USE_XOPEN
__fd_maskfds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->fds_bits)
#else
__fd_mask__fds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->__fds_bits)
#endif
}fd_set;
由以上定义可见,fd_set结构体仅包含一个整形数组,该数组的每个元素的每一位(bit)标记一个文件描述符,fd_set能容纳的文件描述符的数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
由于位操作过于繁琐,我们使用下面的一些列宏来访问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
FD_ISSET(int fd,fd_set *fdset);//测试fdset的位fd是否被设置
timeout参数用来设置select函数的超时时间,它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等了多久,不过我们不能完全信任select调用返回后的timeout值,比如调用失败后timeout 的值是不确定的,timeval的结构体定义如下:
struct timeval
{
long tv_sec;//秒数
long tv-usec;//微秒数
}
由以上定义可见,select给我们提供了一个微秒级的定时方式,如果给timeout变量的tv_sec成员和tv_usec成员都传递为0,则select将立即返回,如果将timeout传递为NULL,则select将一直阻塞,直到某个文件描述符就绪。
select成功返回就绪文件描述符的个数,如果在超时时间内没有任何文件描述符就绪,select将返回0,失败将返回 -1.并设置errno,如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR.
select 的优缺点:
select 的优点:
select模型是Windows sockets中最常见的IO模型。它利用select函数实现IO 管理。通过对select函数的调用,应用程序可以判断套接字是否存在数据、能否向该套接字写入据。
如:在调用recv函数之前,先调用select函数,如果系统没有可读数据那么select函数就会阻塞在这里。当系统存在可读或可写数据时,select函数返回,就可以调用recv函数接 收数据了。
可以看出使用select模型,需要两次调用函数。第一次调用select函数第二次socket API。使用该模式的好处是:可以等待多个套接字。
select缺点:
(1)每次调⽤用select,都需要把fd集合从⽤用户态拷贝到内核态,这个开销在fd很多时会很⼤大
(2)同时每次调⽤用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很⼤大
(3)select⽀支持的⽂文件描述符数量太⼩小了,默认是1024
用例
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/select.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<assert.h>
void Init_fds(int *fds,int len)
{
int i=0;
for(;i<len;++i)
{
fds[i]=-1;
}
}
void Insert_fd(int *fds,int fd,int len)
{
int i=0;
for(;i<len;i++)
{
if(fds[i]==-1)
{
fds[i]=fd;
break;
}
}
}
void Delete_fd(int *fds,int fd,int len)
{
int i=0;
for( ;i<len;++i)
{
if(fds[i]==fd)
{
fds[i]=-1;
break;
}
}
}
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in ser,cli;
memset(&ser,0,sizeof(ser));
ser.sin_family=AF_INET;
ser.sin_port=htons(6000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=bind(sockfd,(struct sockaddr*) &ser,sizeof(ser));
assert(res!=-1);
listen(sockfd,5);
fd_set readfds;//创建读事件,不关注写事件和异常事件,传空
int fds[100];//把所有的文件描述符保存起来
Init_fds(fds,100);
Insert_fd(fds,sockfd,100);
while(1)
{
int maxfd=-1;
FD_ZERO(&readfds);//每次进来都要重新设置
int i=0;
for(;i<100;++i)
{
if(fds[i]!=-1)
{
if(fds[i]>maxfd)
{
maxfd=fds[i];
}
FD_SET(fds[i],&readfds);//设置文件描述符
}
}
int n=select(maxfd+1,&readfds,NULL,NULL,NULL);
if(n<=0)
{
printf("select fail\n");
continue;
}
for(i=0;i<100;++i)
{
if(fds[i]!=-1&&FD_ISSET(fds[i],&readfds))
{ //判断就绪
if(fds[i]==sockfd)
{
//这是有客户链接
int len=sizeof(cli);
int c=accept(sockfd,(struct sockaddr *)&cli,&len);
if(c<0)//判断c
{
printf("onr clienet link error\n");
continue;
}
Insert_fd(fds,c,100);
}
else
{
int fd=fds[i];
char buff[128]={0};
int n=recv(fd,buff,127,0);
if(n<=0)//小于等于零说明客户算关闭
{
close(fd);
Delete_fd(fds,fd,100);//在fds中删掉这个表述符
continue;
}
printf("%d: %s",fd,buff);
send(fd,"ok",2,0);
}
}
}
}
}