Select()在编程中的使用

来源:互联网 发布:收入支出软件 编辑:程序博客网 时间:2024/05/17 03:01

  最近接触到了网络编程,感觉select这个函数还是蛮实用的,也可以让代码显得高端。下面就先来总结下从各位大神汲取的知识,再次消化下。

使用select()理由

  可以使程序进行非阻塞的读写,从而能大大提高程序的执行效率。
  比如在原本应该recv阻塞之前,先加个select来判断是否有数据到来,如果有再进行接收,如果没有则直接跳过。

select函数原型及参数分析

select原型

  int select(int maxfdp,
  fd_set *readfds,
  fd_set *writefds,
  fd_set *errorfds,
  struct timeval*timeout);

select参数分析

  首先同前辈们一样先分析一下select参数中的fd_set和timeval结构体,进而更快理解select中各参数的作用。

fd_set(结构体)

  fd_set是一个文件描述符集合,也可以是一个句柄的集合。
  套用一个经常听的概念,即Unix下一切皆文件,就可以知道select的作用会很大,管道,设备,FIFO等都是文件,当然Socket也是文件,而且一般select与Socket搭配会用的更多。
  而对于fd_set的赋值,我们可以通过宏来进行该结构体的相应操作,如:
  FD_SET( int, fd_set* ): 将一个给定的文件描述符加入集合;
  FD_ZERO( fd_set* ):清空集合,这里需要注意,对文件描述符集合进行设置之前都必须对该文件描述符进行FD_ZERO的操作,即初始化的操作。如果不清空,会导致结果不可知;(具体原因稍后讲到)
  FD_CLR( int, fd_set* ):将一个给定的文件描述符从集合中删除;
  FD_ISSET( int, fd_set* ):检查集合中的文件描述符是否可读写。

timeval(结构体)

  其代表一个时间值。有秒数与毫秒数两个变量,结构体如下:
struct timeval {
time_t tv_sec; /* 秒数 */
suseconds_t tv_usec; /* 毫秒数 */
};

FD_SET等相关宏的阐述

  既然fd_set是结构体,那到底是由哪些变量组成的呢,为什么每次设置赋值前需要清空呢,这些宏到底对fd_set干了什么,又是怎么干的呢?下面将对这些问题进行分析解决。

  首先我们先在程序中通过sizeof()打印出fd_set的大小为128字节,怎么会是128字节,先放着后续说。
  其次了解到fd_set中是每一位表示一个文件描述符。
  最后一个字节8位。
  所以综上所述,fd_set一般最大能保存128(字节) * 8个文件描述符,也就是1024个文件描述符。这个值在Unix中通常会用头文件”sys/select.h”中的FD_SETSIZE来定义,即FD_SETSIZE的值为1024。

  下面引用自一篇博客的说明进行上面宏的问题进行解答,博客地址如下:http://blog.sina.com.cn/s/blog_a43aa27401015kt9.html

为说明方便,取fd_set中的一个1字节进行说明,上面说到fd_set中的每一bit可以对应一个文件描述符fd。那么1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空

  这里可以看出,在每次select之后,没有事件发生的那一位会被清0。同时描述符集合能放多少个,取决于sizeof(fd_set)的值,这也限制了集合的使用数量,那么有什么方法可以突破这个限制吗?可以,下面引用上面博客的一段话进行解释:

  (1)可监控的文件描述符个数取决与sizeof(fd_set)的值。 (2)可以有效突破select可监控的文件描述符上限。
  (3)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
  (4)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。

  下面进入正题,描述处理集合的相关宏的操作:
FD_SET(int x, fd_set* pset):
  指在pset描述集中加入一个新的描述符x;
FD_ZERO(fd_set* pset):
  指清空pset描述集,还是得啰嗦一遍,就是在每次select后,原先没有事件发生的描述位会被清0,所以请在每次执行前进行FD_ZERO;
FD_CLR(int x, fd_set* pset):
  指在描述集中清除第x位;
FD_ISSET(int x, fd_set* pset):
  指判断描述集中的第x位是否发生可读写事件。

  好了讲了这么多回归正题,接下来讲解select中的各个参数的使用。

select参数说明

一、maxfdp:
  为填入描述集总最大的值加一,需要注意这里是最大值加1,不是个数加一;
二、readfds:
  所要监听的读描述集,得到是否有该描述集中的各描述符有的变化,有则返回大于0,没有则根据超时时间进行返回,异常错误返回负值。如果传入NULL的话则表示不关心读变化;
三、writefds:
  所要监听的写描述集,得到释放有该描述集中的各个描述符的变化,有则返回大于0,没有则根据超时时间timeout进行返回,异常错误返回负值,传入NULL表示不关心写变化;
四、errorfds:
  所要监听的错误描述集,同上,表示关系文件的错误异常;
五、timeout:
  超时时间,这里需要注意的是,三种不同timeout的值会产生三中不同的阻塞情况,具体如下所示:
  非阻塞:timeout结构体的秒数和毫秒数均为0,表示select函数不进行阻塞,即非阻塞,当程序执行到select函数时,不管其是否有读写事件或者错误事件发生,都立即返回,返回值当有事件发生时大于0,无事件发生则立即返回0;
  超时阻塞:当timeout结构体中秒数、毫秒数存在值时,表示select函数会进行阻塞,阻塞的时间为timeout设置的时间,当期间有事件发生时即返回大于0,超时时则返回0;
  阻塞:当timeou结构体参数设置为NULL时,表示select函数只有在有事件发生时才会返回大于0,否则一直阻塞。

select返回值

负值:select错误;
大于0的值:描述集中有事件发生变化;
等于0:select函数超时,没有事件发生。

举例

下面引用http://blog.csdn.net/piaojun_pj/article/details/5991968的一个例子来进行使用说明:

①读取键盘输入值,超时间隔2.5秒,输出用户输入的字符个数。

#include <sys/types.h> #include <sys/time.h> #include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <unistd.h> int main() {     char buffer[128];     int result, nread;     fd_set inputs, testfds;     struct timeval timeout;     FD_ZERO(&inputs);//用select函数之前先把集合清零       FD_SET(0,&inputs);//把要检测的句柄——标准输入(0),加入到集合里。     while(1)     {        testfds = inputs;        timeout.tv_sec = 2;        timeout.tv_usec = 500000;        result = select(FD_SETSIZE, &testfds, (fd_set *)0, (fd_set *)0, &timeout);        switch(result)        {        case 0:            printf("timeout/n");            break;       case -1:            perror("select");            exit(1);        default:            if(FD_ISSET(0,&testfds))            {                ioctl(0,FIONREAD,&nread);//取得从键盘输入字符的个数,包括回车。                if(nread == 0)                {                   printf("keyboard done/n");                   exit(0);                }                nread = read(0,buffer,nread);                buffer[nread] = 0;                printf("read %d from keyboard: %s", nread, buffer);          }          break;       }    }    return 0;} 

②利用select而不是fork来解决socket中的多客户问题。

//服务器端:#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <sys/time.h> #include <sys/ioctl.h> #include <unistd.h> int main() {     int server_sockfd, client_sockfd;     int server_len, client_len;     struct sockaddr_in server_address;     struct sockaddr_in client_address;     int result;     fd_set readfds, testfds;     server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket     server_address.sin_family = AF_INET;     server_address.sin_addr.s_addr = htonl(INADDR_ANY);     server_address.sin_port = htons(9734);     server_len = sizeof(server_address);     bind(server_sockfd, (struct sockaddr *)&server_address, server_len);     listen(server_sockfd, 5);     FD_ZERO(&readfds);     FD_SET(server_sockfd, &readfds);//将服务器端socket加入到集合中    while(1)     {        char ch;                        int fd;         int nread;         testfds = readfds;         printf("server waiting/n");         /*无限期阻塞,并测试文件描述符变动 */        result = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0);         if(result < 1)         {             perror("server5");             exit(1);         }         /*扫描所有的文件描述符*/        for(fd = 0; fd < FD_SETSIZE; fd++)         {            /*找到相关文件描述符*/            if(FD_ISSET(fd,&testfds))             {                         /*判断是否为服务器套接字,是则表示为客户请求连接。*/                if(fd == server_sockfd)                 {                     client_len = sizeof(client_address);                     client_sockfd = accept(server_sockfd,                     (struct sockaddr *)&client_address, &client_len);                     FD_SET(client_sockfd, &readfds);//将客户端socket加入到集合中                    printf("adding client on fd %d/n", client_sockfd);                 }                 /*客户端socket中有数据请求时*/                else                 {                                                              ioctl(fd, FIONREAD, &nread);//取得数据量交给nread                    /*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */                    if(nread == 0)                     {                         close(fd);                         FD_CLR(fd, &readfds);                         printf("removing client on fd %d/n", fd);                     }                     /*处理客户数据请求*/                    else                     {                         read(fd, &ch, 1);                         sleep(5);                         printf("serving client on fd %d/n", fd);                         ch++;                         write(fd, &ch, 1);                     }                 }             }         }     } } 
//客户端#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h>                                                                    #include <arpa/inet.h> #include <unistd.h> int main() {     int client_sockfd;     int len;     struct sockaddr_in address;//服务器端网络地址结构体                                                int result;     char ch = 'A';     client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket                                   address.sin_family = AF_INET;     address.sin_addr.s_addr = inet_addr(“127.0.0.1”);                 address.sin_port = 9734;     len = sizeof(address);     result = connect(client_sockfd, (struct sockaddr *)&address, len);     if(result == -1)     {          perror("oops: client2");          exit(1);     }     write(client_sockfd, &ch, 1);     read(client_sockfd, &ch, 1);     printf("char from server = %c/n", ch);     close(client_sockfd);     zexit(0); }                      
0 0