Linux下socket多路复用应用--select函数

Linux下socket多路复用应用--select函数
Select系统调用是用来让我们的程序监视多个文件描述符(file descriptor)的状态变化的。程序会停在select这里等待,直到被监视的文件描述符有某一个或多个发生了状态改变。
文件描述符在Linux里有很多,如果你man某个函数,在函数返回值部分说到成功后有一个文件描述符被创建,如man socket可以看到“On success,a file descriptor for the new socket is returned.”而man 2 open可以看到“open() and create() return the new file descriptor”,其实文件描述符就是一个整数,看socket函数的声明就明白了:
Int socket(int domain, int type,int protocol);
Linux下socket多路复用应用--select函数
文章图片

Linux下socket多路复用应用--select函数
文章图片

当然,我们最熟悉的文件描述符是0,1,2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE*结构的表示就是stdin、stdout、stderr,0就是stdin,1就是stdout,2就是stderr。
select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一个打开的文件描述符(不管是socket,还是其他文件或命名管道或设备描述符)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select的进程哪一个socket或文件可读。
非阻塞式I/O编程有两个特点:
1.如果一个发现I/O有输入,读取的过程中,另外一个也有了输入,这时候不会产生任何反应。这就需要你的程序语句去用到select函数的时候才知道有数据输入。
2.程序调用select的时候,如果没有数据输入,程序会一直等待,直到有数据为止,也就是程序中无需循环和sleep。
select在socket编程中还是比较重要的,可是对于初学socket的人来说都不太爱用select写程序,他们只是习惯写诸如connect、accept、recv或recvfrom这样的阻塞程序(所谓非阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果某个事件没有发生,进程或线程就被阻塞,函数不能立即返回)。
可是使用select就可以完成非阻塞(所谓非阻塞方式non_block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映此函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生,则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况--读写或是异常。

Select函数介绍
函数名:select
头文件:#include
#include
#include
函数原型:int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
函数功能:检测文件状态变化
参数说明:1.ndfs:select监视的文件描述符数,视进程中打开文的文件数而定,一般设定为你要监视各文件中最大文件号加一。
2.readfds:select监视的可读文件描述符集合。
3.writefds:select监视的可写文件描述符集合。
4.exceptfds:select监视的异常文件描述符集合。
5.timeout:本次select()的超时结束时间。(见/usr/sys/select.h)
struct timeval
{
long tv_sec; //秒
long tv_unsec; //微秒
};(可精确至百万分之一秒)
函数返回值:成功时,返回准备就绪的描述符数,若超时则返回0,失败则返回-1。

我们在程序中药申明几个fd_set类型的变量,比如rdfds,wtfds,exfds,然后把这个变量的地址&rdfds,&wtfds,&exfds传递给select函数。这三个参数都是一个描述符的集合,第一个rdfds是用来保存这样的描述符的:当描述符的状态变成可读的时系统就会告诉select函数返回,同理第二个wtfds是指有描述符状态变成可写是系统就会告诉select函数返回,同理第三个参数exfds是特殊情况,即描述符有特殊情况发生时系统就会告诉select函数返回。对socket编程有用的就是readfds。
几个相关的宏解释如下:
FD_ZERO(fd_set* fdset):清空fdset与所有文件描述符的联系。
FD_SET(int fd,fd_set* fdset):建立文件描述符fd与fdset的联系。
FD_CLR(int fd,fd_set* fdset):清除文件描述符fd和fdset的联系。
FD_ISSET(int fd,fd_set* fdset):检查fdset联系的文件描述符fd是否可读写,当>0表示可读写。
(关于fd_set及相关宏的定义见/usr/include/sys/types.h)。
特殊情况比如对方通过一个socket描述符发来了紧急数据。如果我们程序里只是想检测某个socket是否有数据可读,我们可以这样:

fd_set rdfds; //先声明一个fd_set集合来保护我们要检测的socket描述符
struct timeval tv; //声明一个时间变量来保存时间
int ret; //保存返回值
FD_ZERO(&rdfds); //用select函数之前先把集合清零
FD_SET(socket,&fdfds); //把要检测的描述符socket加入到集合里
tv.tv_sec = 1;
tv.tv_usec = 500; //设置select等待的最大时间为1秒加500毫秒
ret = select(socket+1,&rdfds,NULL,NULL,&tv); //检测我们上面设置到集合rdfds里的描述符是否有可读信息
If(ret < 0 )perror(“select”); //这说明select函数出错
else if(ret == 0)printf(“超时\n”); //说明在我们设定的时间值1秒加500毫秒内,socket的状态没有发生变化
else //说明等待时间内,socket的状态发生了变化
{
printf(“ret = %d\n”,ret); //ret这个返回值记录了发生状态变化的描述符的数目,由于这里我们只监视了socket这一个描述符,所以这里一定ret == 1,如果同时有多个描述符发生变化返回的就是描述符的总和了
//这里我们就可以从socket这个描述符里读取数据了,因为select函数已经告诉我们这个描述符里有数据可读
if(FD_ISSET(socket,&rdfds)) //先判断一下socket这被监视的描述符是否真的变成可读的了
{
recv() ; //读取socket描述符里的数据
}
}

【Linux下socket多路复用应用--select函数】注意:select函数的第一个参数,是所有加入集合的描述符值得最大那个值还要加1。比如我们创建了3个描述符:
int sa,sb,sc;
sa = socket(...);
connect(sa,...);
sb = socket(...);
connect(sb,...);
sc = socket(...);
connect(sc,...);
FD_SET(sa,&rdfds);
FD_SET(sb,&rdfds);
FD_SET(sc,&rdfds);
在使用select函数之前,一定要找到3个描述符的最大值,我们一般定义一个变量来保存最大值,取得最大socket值,算法就不用说了吧,求三个数的最大值就OK了。定义最大值变量int maxfd = 0;
。。。
ret = select(maxfd+1,&rdfds,NULL,NULL,&tv);
同样的道理,如果我们要检测用户是否按了键盘进行输入,我们就应该把标准输入0这个描述符放到select里来检测,如下:
FD_ZERO(&rdfds);
FD_SET(0,&rdfds);
tv.tv_sec = 1;
tv.tv_usec = 0;
ret = select(1,&rdfds,NULL,NULL,&tv); //注意最大值还要加1
if(ret < 0)
perror(“select”);
else if(ret == 0)
printf(“超时\n”);
else
scanf(“%s”,buf);

Ok,用法已经讲的很明白了吧,接下来就是select的具体应用了,如果有兴趣的到时候可以看一下我的“TCP套接字编程实例(三)”,里面讲了利用select和TCP套接字实现异步通讯聊天。

    推荐阅读