多路复用之select

   之前在套接字编程中我们用了多线程和多进程的方法来编写,用它们编写的好处自然是稳定,而却非常耗资源,在前面的高级I/O博客中说到了另外一种方式那便是高效的多路复用方式了,它会一次等待多个满足条件的文件描述符,这里先介绍第一种多路复用方式select后面还会介绍比它更好的epoll和最好的多路复用方式epoll。


select的多路复用方式是这样的:它需要一个buf来存放需要监听的文件描述符,先要把所关心的文件描述符放入到所对应的读事件集合或者写事件集合中,一旦其中发生变化会以参数反应出来。

先来看看select的函数

       /* According to POSIX.1-2001 */
       #include <sys/select.h>

       /* According to earlier standards */
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);
       
          struct timeval {
               long    tv_sec;         /* seconds */
               long    tv_usec;        /* microseconds */
           };

nfds:是所要监控的文件描述符的个数加一,注意,一定要加一!!!

fd_set *readfds:表示监控的readfds集,这个fd_set是有上限的,我的机子上是128,那么表示最多能监控128*8=1024个。readfds是输入输出型的参数,它的每个bit为会对应监控一个文件描述符,select是阻塞式的等待事件的发生的,当某个文件描述符就绪则把它对应的bit为置1,而其它的都要清空,所以每次在select之前都要先执行emty(下面会说到),和set(下面会说到)。

writeds:写事件文件描述符集

exceptfds:错误事件文件描述符集

timeout:设置等待事件,等待事件到了可以做其他的事情,比如提示。。

FD_CLR清空所设置关心的文件描述符集

FD_ISSET:用于检查该文件描述符是否在对应集合中

FD_SET:用于将所关心的文件描述符添加入对应集合中

FD_ZERO:将对应文件描述符集中的所有bit位置0


下面是我对select的工作流程理解

wKioL1dNjSyyqiIoAADP_SMQT0I554.png

下面是一个使用select实现的TCPserver的简单代码

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/socket.h>
  4 #include<sys/select.h>
  5 #include<arpa/inet.h>
  6 #include<netinet/in.h>
  7 #include<string.h>
  8 #include<unistd.h>
  9 
 10 
 11 int fd[64];   //select需要创建一个用于存放socket文件描述符的buf
 12 void usage(char *proc)
 13 {
 14     printf("%s+[ip]+[port]\n",proc);
 15 }
 16 
 17 int listen_sock(const char*ip,int port)
 18     //创建listen套接字前面的socket博客中已经详细说明了
 19 {
 20     int sock=socket(AF_INET,SOCK_STREAM,0);
 21     if(sock<0)
 22     {
 23         perror("socket");
 24     }
 25     struct sockaddr_in server;
 26     server.sin_family=AF_INET;
 27     server.sin_addr.s_addr=inet_addr(ip);
 28     server.sin_port=htons(port);
 29     if(bind(sock,(struct sockaddr*)&server,sizeof(server))<0)
 30     {
 31 
 32         perror("bind");
 33     }
 34     if(listen(sock,10)<0)
 35     {
 36         perror("listen");
 37     }
 38     return sock;
 39 }
 40 
 41 void select_server(int sock)
 42 {
 43     int i=0;
 44     for(;i<64;i++)  //初始化为无效的文件描述符
 45     {
 46         fd[i]=-1;
 47     }
 48     fd_set reads;  //定义读文件描述符集
 49     printf("%d\n",sizeof(fd_set));
 50     fd_set writes; //定义写文件描述符集
 51     int max_fd;    //定义select所需要的参数,比最大的文件描述符大1
 52     int newsock=-1;//定义所需要的accept到的文件描述符
 53     int fd_len=sizeof(fd)/sizeof(fd[0]);//select参数
 54 
 55     struct sockaddr_in routom;
 56     socklen_t routom_len=sizeof(routom);
 57 
 58     struct timeval timeout;//设置最大等待时间用于提醒
 59     fd[0]=sock;    //将buff中的第一个设置为监听套接字用于监听
 60     max_fd=sock;   //目前sock是最大的文件描述符
 61 
 62     char buf[1024];
 63     while(1)
 64     {
 65         timeout.tv_sec=5;     //单位为秒
 66         timeout.tv_usec=0;    //单位为毫秒
 67         FD_ZERO(&reads);      //初始化
 68         FD_ZERO(&writes);
 69         FD_SET(sock,&reads);
 70         //将sock添加到reads读文件描述符集中因为每次我们都需要这个文件描述符
 71         int i=0;
 72         for(;i<fd_len;i++)
 73         {
 74         //寻找有效的文件描述符并添加到reads集中,注意(因为我们所要实现的是
 75         //客户端发消息,服务器显示,所以我们不关心写文件描述符,只有两种我们
 76         //需要关心的文件描述符1.accept到的需要链接服务器的文件描述符.2.有数据
 77         //需要去读的问家描述符。
 78             if(fd[i]>0)
 79             {
 80                 FD_SET(fd[i],&reads);
 81                 if(fd[i]>max_fd)
 82                 {
 83                     max_fd=fd[i];           //将最大的文件描述符给max_fd
 84                 }
 85             }
 86         }
 87         switch(select(max_fd+1,&reads,&writes,NULL,&timeout)){
 88                 //正式进入select函数
 89                 case -1:
 90                     perror("select");
 91                 case 0 :    //返回值为0代表timeout了
 92                     printf("timeout...\n");
 93                     break;
 94                 default:
 95                     {
 96                      i=0;
 97                     for(;i<fd_len;i++)
 98                     {
 99 
100                         if(fd[i]==sock&&FD_ISSET(fd[i],&reads))
101                         //每次我们都需要监听一次
102                         {
103 
104                             newsock=accept(sock,(struct sockaddr*)\
105                                             &routom,&routom_len);
106                             printf("%d\n",newsock);
107                             if(newsock<0)
108                             {
109                                 perror("accept");
110                                 continue;
111                             }
112                             for(i=0;i<fd_len;i++)
113                             //监听到了就把这个套接字给一个有效的buf空间
114                             {
115                                 if(fd[i]==-1)
116                                 {
117                                     fd[i]=newsock;
118                                     printf("[ip]:%s has comming...\n",\
119                                     inet_ntoa(routom.sin_addr));
120                                     break;
121                                 }
122                             }
123                             if(i==max_fd)
124                             //如果满了就先关闭他
125                             {
126                                 close(newsock);
127                             }
128                         }
129                         else if(fd[i]>0&&FD_ISSET(fd[i],&reads))
130                         //满足有数据要读的文件描述符
131                         {
132                             memset(buf,'\0',sizeof(buf));
133                             ssize_t size=read(fd[i],buf,sizeof(buf)-1);
134                             if(size>0)
135                             {
136                                 buf[size]='\0';
137                                 printf("[%s]::%s\n",inet_ntoa\
138                                 (routom.sin_addr),buf);
139                             }else if(size==0){
140                             //size小于零时,则说明远端已经关闭
141                                 printf("[ip]%s:is shutdown\n",\
142                                 inet_ntoa(routom.sin_addr));
143                                 close(fd[i]);
144                                 fd[i]=-1;
145                             }else{
146                                     ;
147                             }
148                         }
149                     }
150                 }
151             }
152     }
153 }
154 
155 int main(int argc,char *argv[])
156 {
157     if(argc!=3)
158     {
159         usage(argv[0]);
160     }
161     char *ip=argv[1];
162     int port=atoi(argv[2]);
163     int sock=listen_sock(ip,port);
164 
165     select_server(sock);
166     close(sock);
167     return 0;
168 }


总结:select可以一次监听多个事件,但是它的缺点是很明显的。

    1、它是有监听上限的,我的电脑上限是1024个。

    2、它是通过输出型参数来返回的,这样我们就不得不每次监听到都要遍历        一次整个存放文件描述符的buf。

    3、它会在设置的时候在用户态和内核态中来回copy这样开销很大。