什么是I/O复用:
程序设计过程中有时候会对多个输入进行操作,例如标准输入或者多个socket。那么如果在对某个进行处理的时候阻塞住的话,另外的输入进来的话会得不到及时的处理。(比如客户端程序调用fgets/cin的时候会阻塞在标准输入的地方,此时如果有监听的socket请求连接的话是得不到及时处理的)。对于这种情况就需要当内核发现我们指定的IO条件就绪的时候主动的通知进程。这种行为就是IO复用,(个人认为复用的是进程而不是IO)。
即使得程序可以同时监听多个文件描述符。
针对如下场合就需要用到IO复用:
- 当客户处理多个描述符时候
- 一个同时处理多个套接字的时候
- 如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字的时候
- 如果一个服务器既要处理TCP又要处理UDP
- 如果一个服务器要处理多个服务或者多个协议的时
文件描述符:内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
select()函数:
函数原型:
#include<sys/select.h>
#include<sys/time.h>
int select(int maxfd1, fd_set *readset, fd_set *writeset, fd_set *exceptset,
const struct timeval *timeout);
返回:若有就绪描述符则为其数目,若超时则返回0,出错返回-1
参数:
- 第一个参数:指定被监听的文件描述符的总数,通常被设置为select监听的所有文件描述符中的最大值加1。描述符0,1,2,...,直到maxfdp1 - 1均被测试。比方说我们现在关心的描述符是1,4,7 那么这个参数应该传入8。这个参数完全是为了效率原因,我们告诉内核这个参数那么内核在进程与内核之间就无需复制描述集中不必要的部分,从而不测试那些总是为0的位,提高了效率。
- 三个参数readset,writeset,exceptset指定我们要让内核测试读、写和异常条件的描述符。
- 参数timeout指定内核等待的时间,其结构如下:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
}
该参数有三种可能:1.永远等待下去:仅在有一个描述符准备好I/O时才返回,将其设为空指针
2.等待一段固定时间:在有一个描述符准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
3.根本不等待:检查描述符后立即返回,这就是轮询。为此,该参数必须指向一个timeval结构,但是其中的值必须设置为0
对于上面说到的fd_set类型有如下几个操作函数(fd_set结构体仅包含一个整形数组,该数组的每个元素的每一位标记一个文件描述符):
Void FD_ZERO(fd_set *fdset); /* clear all bits in fdset */
Void FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fdset */
Void FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fdset */
int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset *///返回非0 表示可读写
实例代码:
#include<stdio.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
int main(void){
fd_set rfds;
struct timeval tv;
int retval;
char temp[100];
/*Watch stdin (fd 0) to see when it has input.*/
FD_ZERO(&rfds);
FD_SET(0,&rfds);
/*Wait up to five seconds.*/
tv.tv_sec = 5;/*seconds*/
tv.tv_usec = 0;/*microseconds*/
retval = select(1,&rfds,NULL,NULL,&tv)
/*Don't rely on the value of tv now!*/
if(retval)
{
fgets(temp,100,stdin);
printf("Data is available now.\n");
}
/*FD_ISSET(0,&rfds) will be true.*/
else
{
printf("No data within five seconds.\n");
}
exit(0);
}
I/O复用例程:
聊天程序。双方都可以从终端输入一串字符(以回车结束),通过UDP的方式发送到对方,并显示在对方的终端上。从命令行输入目的地址、目的端口、源地址、源端口。
问题所在:双方都要读取Socket数据和标准输入数据。即调用read和fgets函数。此两个函数都会引起进程阻塞。例如:调用fgets,进程阻塞等待用户输入数据,此时对方向自已发送数据….不能处理!
解决方法,使用I/O复用,进程阻塞到select,当标准输入和socket有数据时返回。
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>//read和write函数的头文件
#include <errno.h>
#define BUFLEN 255
#define max(x,y) (((x)>(y))?(x):(y))
int main(int argc,char**argv)
{
struct sockaddr_in peeraddr,localaddr;
int sockfd,n,maxfd,socklen;
char msg[BUFLEN+1];
fd_set infds; //fd文件描述符
if(argc!=5)
{
printf("%s<dest IP address><dest port><source IP address> <source port>\n",argv[0]); //目的地址 目的端口 源地址 源端口
exit(0);
}
else
{
sockfd=socket(AF_INET,SOCK_DGRAM,0); //创建UDP套接字
if(sockfd<0)
{
fprintf(stderr,"socket creating error in udptalk.c\n");
exit(1);
}
socklen=sizeof(struct sockaddr_in);
memset(&peeraddr,0,socklen);//初始化目的地址
peeraddr.sin_family=AF_INET;//IPv4
peeraddr.sin_port=htons(atoi(argv[2]));//设置端口 atoi()端口转化
if(inet_pton(AF_INET,argv[1],&peeraddr.sin_addr)<=0)//inet_pton() ip地址转化,将字符串转化成点十进制的ip地址形式,并存入peeraddr.sin_addr中
{
printf("wrong dest ip address\n");
exit(0);
}
memset(&localaddr,0,socklen); //初始化本地地址 将localaddr所指向的某一块内存中的后socklen个字节的内容全部设置为指定的ASCII值
localaddr.sin_family=AF_INET;
if(inet_pton(AF_INET,argv[3],&localaddr.sin_addr)<=0)
{
printf("Wrong source IP address\n");
exit(0);
}
localaddr.sin_port=htons(atoi(argv[4]));
if(bind(sockfd,(struct sockaddr *)&localaddr,socklen)<0) //绑定本机
{
fprintf(stderr,"bind local address error in udptalk.c\n");
exit(2);
}
connect(sockfd,(struct sockaddr*)&peeraddr,socklen); //利用绑定的主机连接目的主机
for(;;)
{
FD_ZERO(&infds);/* clear all bits in fdset */
FD_SET(fileno(stdin),&infds); /* turn on the bit for fd in fdset */
FD_SET(sockfd,&infds);/* turn on the bit for fd in fdset */
maxfd=max(fileno(stdin),sockfd)+1;
if(select(maxfd,&infds,NULL,NULL,NULL)==-1) //maxfd为最大文件描述符
{
fprintf(stderr,"select error in udptalk.c\n");
exit(3);
}
if(FD_ISSET(sockfd,&infds))//返回非0表示可读
{
n=read(sockfd,msg,BUFLEN);
if((n==-1)||(n==0))
{
printf("peer closed\n");
exit(0);
}
else
{
msg[n]=0;
printf("peer:%s",msg);
}
}
if(FD_ISSET(fileno(stdin),&infds))
{
if(fgets(msg,BUFLEN,stdin)==NULL)
{
printf("talk over!\n");
exit(0);
}
write(sockfd,msg,strlen(msg));
printf("sent:%s",msg);
}
}
}
}