1、I/O复用:解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的 I/O 系统调用。
2、I/O复用适用于:
a. 客户端程序要同时处理多个socket。
b. 客户端程序要同时处理用户输入和网络连接。
c. TCP服务器既要处理监听套接字,又要处理已连接套接字。
d. 服务器要同时处理TCP和UDP请求。
e. 服务器要监听多个窗口或者处理多种服务
I/O复用常用函数:select、poll、epoll
3、select()函数
系统调用原型:
#include
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *execptfds,struct timeval *timeout);
功能:轮询扫描多个描述符中的任一描述符是否发生响应,或经过一段时间后唤醒。
参数:
a. nfds: 指定要检测的描述符的范围,所检测描述符最大值+1
b. readset:可读描述符集,监测该集合中的任意描述符是否有数据可读
c. writeset:可写描述符集,监测该集合中的任意描述符是否有数据可写
d. exceptset:异常描述符集,监测该集合中的任意描述符是否发生异常
e. timeout:超时时间,超过规定时间后唤醒
返回值为负值表示select错误,正值表示某些文件可读写或出错 ,0表示等待超时,没有可读写或错误的文件
以下为服务器端select.c与多个客户端cli.c通过I/O复用单线程建立连接的程序。
select.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXFD 10
void fds_init(int fds[])//数组初始化
{
int i = 0;
for(; i < MAXFD; i++)
{
fds[i] = -1;
}
}
void fds_add(int fds[],int fd)//添加元素到数组
{
int i = 0;
for(; i < MAXFD;i++)
{
if(fds[i] == -1)
{
fds[i] = fd;
break;
}
}
}
void fds_del(int fds[],int fd)//逻辑上移除一个元素
{
int i = 0;
for(; i < MAXFD ; 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 saddr,caddr;
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));
assert(res != -1);
listen(sockfd,5);
int fds[MAXFD];//定义一个要添加元素的数组
fds_init(fds);
fds_add(fds,sockfd);//添加唯一一个有效的元素sockfd
while(1)
{
fd_set fdset;//定义集合
FD_ZERO(&fdset;//将集合清空,每个位置置为0
//循环添加元素到集合
int maxfd = -1;//定义描述符最大值
int i = 0;
for(;i < MAXFD;i++)//找描述符最大值
{
if(fds[i] == -1)//无效描述符
{
continue;
}
//有效就添加
FD_SET(fds[i],&fdset);
if(maxfd < fds[i])
{
maxfd = fds[i];
}
}
struct timeval tv = {5,0};//设置超时时间
//把集合交给select处理
int n = select(maxfd+1,&fdset,NULL,NULL,&tv);
//maxfd+1:描述符最大值+1,表示告诉select只需要关注前maxfd+1个就好了,
//后面的不用关注,因为未用到;
//fdset:读事件集合(对方发数据);写;异常;tv置为空,则表示永久阻塞
if(n == -1)//失败
{
perror("select error\n");
continue;
}
else if(n == 0)//超时,5s时间到,没有生成就绪队列或者无客户发数据
{
printf("time out\n");
continue;
}
//到此只知道有几个文件描述符,但是不清楚是哪个描述符上的数据已就绪
else
{
int i = 0;
for(;i < MAXFD;i++)//遍历整个数组
{
if(fds[i] == -1)//无效描述符
{
continue;
}
//判断描述符上有没有数据就绪,如果为真,有数据就绪,就处理
if(FD_ISSET(fds[i],&fdset))
{
if(fds[i] == sockfd)
{
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c < 0)
{
continue;
}
printf("accept c = %d\n",c);
//每建立一个新连接就会增加一个新的描述符,则将新得到的添加到数组中,
//在下次select循环时集合中就会多个,方便处理
fds_add(fds,c);
}
else
{
char buff[128] = {0};
int num = recv(fds[i],buff,127,0);
if( num<= 0)//对方关闭了
{
close(fds[i]);
fds_del(fds,fds[i]);//移除
printf("one client over\n");
}
else //说明读到了num个数据
{
printf("recv(%d)=%s\n",fds[i],buff);
send(fds[i],"OK",2,0);
}
}
}
}
}
}
}
cli.c
运行结果:
例:程序main.c关注键盘,有数据输入就读取打印,没有数据输入就timeout。
#include
#include
#include
#include
#include
#include
#include
#define STDIN 0
int main()
{
int fd = STDIN;
char buff[128] = {0};
while(1)
{
fd_set fdset;
FD_ZERO(&fdset);
DS_SET(fd, &fdset);
struct timeval tv = {5, 0};
int n = select(STDIN+1, &fdset, NULL, NULL, &tv);
if(n < 0)
{
continue;
}
else if(n == 0)
{
printf("time out\n");
continue;
}
else
{
if(FD_ISSET(fd,&fdset))
{
read(fd, buff, 127);
if(strncmp(buff,"end",3)==0)
{
break;
}
printf("read = %s\n", buff);
}
}
}
}
运行结果: