【Unix 网络编程】服务器网络编程模型——I/O复用:select 函数

来源:互联网 发布:统计数据软件 编辑:程序博客网 时间:2024/05/21 09:01

一、select 函数

I/O复用是指内核一旦发现进程指定的一个或多个I/O条件就绪,它就通知该进程。I/O复用主要用于网络应用,典型使用在一下场合:

1.当客户处理多个描述字(通常是交互式输入和网络套接口)时,必须使用I/O复用;

2.一个客户同时处理多个套接口时;

3.如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般就要使用I/O复用;

4.如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用;

5.如果一个服务器要处理多个服务器或者多个协议,一般就要使用I/O复用。

I/O复用,先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行I/O时,该函数返回,在返回时,它告诉进程哪些描述符已准备好进行I/O。


我们先学习 select 函数。该函数允许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

我们调用 select 告知内核对哪些描述字(就读、写或异常条件)感兴趣以及等待多长时间。任何描述字都可以使用 select 来测试。函数原型如下

#include <sys/select.h>#include <sys/utime.h>int select(int maxfdp1, fd_set *readset, fd_set *writeset,       fd_set *exceptset, const struct timeval *timeout);//返回:就绪描述符字的正数目,0——超时,-1——出错
select 的第一个参数 maxfdp1 指定待测试的描述字个数,它的值是待测试的最大描述符加1(因此我们把该参数命名为maxfdp1),因为描述符是从0开始的,如果我们将这个参数设置为我们所关注的最大描述符编号值加1,内核就只需在此范围内寻找打开的位,而不必在三个描述符集中的数百位内搜索。

中间三个参数 readset、writeset 和 exceptset 指定我们要让内核测试读、写和异常条件的描述字。select 使用描述字集,每个描述符集存放在一个 fd_set 数据类型中。可以通过以下四个宏对其进行设置:

#include <sys/select.h>void FD_ZERO(fd_set *fdset);            //清空集合内的所有位void FD_SET(int fd, fd_set *fdset);     //将一个文件描述字添加到集合中void FD_CLR(int fd, fd_set *fdset);     //将一个文件描述字从集合中删除int FD_ISSET(int fd, fd_set *fdset);    //测试该集合中的指定位是否已准备好//返回:若fd在描述符集中则返回非0值,否则返回0
最后一个参数则是告知内核它等待所指定描述字中任何一个就绪可花多长时间,其 timeval 结构用于指定这段时间的秒数和微秒数。

struct timeval{long tv_sec;  /*seconds*/long tv_usec; /*microseconds*/};
这个参数有三种可能:

timeout = NULL :永远等待,仅在有一个描述字准备好 I/O 时才返回;

timeout->tv_sec ==0 && timeout->tv_usec == 0 :完全不等待,测试所有指定的描述符并立即返回,成为轮询

timeout->tv_sec !=0 && timeout->tv_usec != 0 :等待一段固定时间,当有描述符准备好或是指定的时间已经超过时立即返回。


从上面的介绍可知,我们调用 select 函数,需要声明一个描述符集,然后用 FD_ZERO 清除其所有位,然后在其中设置我们关心的各个位(FD_SET),然后调用select,从select 返回时,用 FD_ISSET 测试该集中的一个给定位是否仍旧设置。

select 函数修改由指针 readset、writeset 和 exceptset 所指向的描述字集,这三个参数都是值-结果参数,调用该函数时,我们指定所关心的描述字的值,该函数返回时,结果指示哪些描述字已就绪


select 函数有三个可能的返回值:

返回值-1表示出错,例如在所指定的描述符都没有准备好时捕捉到一个信号,在此种情况下,将不修改其中任何描述符集;

返回值0表示没有描述符准备好。若指定的描述符都没有准备好,而且指定的时间已经超过,则发生这种情况。此时,所有描述符集皆被清0。

正返回值表示已经准备好的描述符数,该值是三个描述符集中已准备好的描述符数之和,在这种情况下,三个描述符集中仍旧打开的位对应于已准备好的描述符。

上面的“准备好”有必要说下,在程序测试的时候,本来先由客户端发送(write)的,结果在程序中设置了一直等待读描述符集,导致select一直阻塞等待。

  • 若对读集(readfds)中的一个描述符的 read 操作将不会阻塞,则此描述符是准备好的。
  • 若对写集(writefds)中的一个描述符的 write 操作将不会阻塞,则此描述符是准备好的。
  • 若对异常状态集(exceptfds)中的一个描述符有一个未决异常状态,则此描述符是准备好的。

二、测试程序

这里对之前的 socket 程序 进行修改。

服务器端:

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<sys/socket.h>#include<sys/types.h>#include<unistd.h>#include<netinet/in.h>#include <errno.h> #include <sys/select.h>#define PORT 6665int main(int argc,char **argv){int ser_sockfd,cli_sockfd;int err,n,i;int addrlen;struct sockaddr_in ser_addr;struct sockaddr_in cli_addr;char recvline[200],sendline[200];//selectfd_set readset,allset;int maxfd;int fdready;int fdnum;ser_sockfd=socket(AF_INET,SOCK_STREAM,0);          //创建套接字if(ser_sockfd==-1){printf("socket error:%s\n",strerror(errno));return -1;}bzero(&ser_addr,sizeof(ser_addr));/*在待捆绑到该TCP套接口(ser_)*/ser_addr.sin_family=AF_INET;ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);ser_addr.sin_port=htons(PORT);err=bind(ser_sockfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));   //捆绑if(err==-1){printf("bind error:%s\n",strerror(errno));return -1;}err=listen(ser_sockfd,5);                                      //监听地址 if(err==-1){printf("listen error\n");return -1;}printf("listen the port:\n");//select functionFD_ZERO(&allset);FD_SET(ser_sockfd,&allset);int clientfds[FD_SETSIZE];maxfd = ser_sockfd;for(i = 0; i < FD_SETSIZE; ++i) //多个客户端情况{clientfds[i] = -1;}while(1){readset = allset;fdready = select(maxfd+1,&readset,NULL,NULL,NULL);   //这里的服务器端一开始是接收数据if(-1 == fdready){perror("select error:");exit(1);}        if(FD_ISSET(ser_sockfd, &readset))   //测试监听描述字是否已经准备好{    addrlen = sizeof(struct sockaddr);   //准备好则进行连接cli_sockfd = accept(ser_sockfd,(struct sockaddr *)&cli_addr, &addrlen);if(-1 == cli_sockfd){perror("accpet error:");exit(1);}for(i = 0; i < FD_SETSIZE; ++i){if(clientfds[i] < 0)   //将客户端描述字添加到队列{clientfds[i] = cli_sockfd;break;}}fdnum = (i > fdnum ? i : fdnum);if(i == FD_SETSIZE){perror("too many clients:");exit(1);}FD_SET(cli_sockfd, &allset);maxfd = (cli_sockfd > maxfd ? cli_sockfd : maxfd);//事实上这里select设置的是一直等待,如果不是这样,这里需要continue回去继续select}        for(i = 0; i <= fdnum; ++i){if(clientfds[i] < 0)   //找到连接后的描述字  continue;if(FD_ISSET(clientfds[i], &readset)) //测试是否可读{printf("waiting for client...\n");n = recv(clientfds[i], recvline, 1024, 0);if(0 == n){close(clientfds[i]);FD_CLR(clientfds[i], &allset);clientfds[i] = -1;continue;}recvline[n] = '\0';printf("recv data is:%s\n", recvline);printf("input your words:");scanf("%s", sendline);send(clientfds[i], sendline, strlen(sendline), 0);}}}return 0;}

客户端:

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<sys/socket.h>#include<sys/types.h>#include<unistd.h>#include<netinet/in.h>#include<sys/select.h>#define PORT 6665int main(int argc,char **argv){int sockfd, fdready;int err,n;struct sockaddr_in addr_ser;char sendline[20],recvline[20];fd_set writeset;sockfd=socket(AF_INET,SOCK_STREAM,0);       //创建套接字 if(sockfd==-1){printf("socket error\n");return -1;}bzero(&addr_ser,sizeof(addr_ser));     /**/     addr_ser.sin_family=AF_INET;addr_ser.sin_addr.s_addr=htonl(INADDR_ANY);   addr_ser.sin_port=htons(PORT);                err=connect(sockfd,(struct sockaddr *)&addr_ser,sizeof(addr_ser));    if(err==-1){printf("connect error\n");return -1;}printf("connect with server...\n");FD_ZERO(&writeset);while(1){printf("sockfd = %d\n", sockfd);FD_SET(sockfd, &writeset);fdready = select(sockfd+1, NULL, &writeset, NULL, NULL);  //此处是写描述符集if(-1 == fdready){perror("select error:");exit(1);}if(FD_ISSET(sockfd, &writeset)){printf("Input your words:");scanf("%s",sendline);send(sockfd,sendline,strlen(sendline),0);           //客户端-->服务端printf("waiting for server...\n");n=recv(sockfd,recvline,100,0);                     //客户端<--服务端recvline[n]='\0'; printf("recv data is:%s\n",recvline);}}return 0;}

回看上面的程序,在服务器端与客户端建立连接之后,服务器端的 select 是设置的读描述符集,因为这里是在连接之后,客户端先发送消息,然后服务器端接受消息并回执消息,如此反复。前面对于“准备好”进行了阐述,服务器端 select 关心的是读描述集(readfds),如果对该集中的一个描述符(套接字描述字)的 read 操作不会阻塞(有数据可读,也就是客户端有数据写入,就不会阻塞),则此描述符是准备好的,不会阻塞在 select 处(这里设置的一直等待)。同样在客户端,如果也设置读描述符集,那么客户端将会一直阻塞在 select 处,因为该描述符字没有数据写入,读自然会阻塞,读描述符集没有准备好,select 就一直等待。


0 0
原创粉丝点击