学科分类
目录
Linux编程

select

使用select搭建的多路I/O转接服务器是一种基于非阻塞的服务器:当有客户端连接请求到达时,accept会返回一个文件描述符,该文件描述符会被存储到由select监控的文件描述符表中,每个文件描述符对应的文件都可进行I/O操作,因此select可通过监控表中各个文件描述符,来获取对应的客户端I/O状态。若每路程序中都没有数据到达,线程将阻塞在select上;否则select将已就绪客户端程序的数量返回到服务器。基于select的通信模型如图1所示。

图1 select通信模型示意图

Linux系统的select机制中提供了一个名为select的系统调用,该函数存在于函数库sys/select.h中,用于监视客户端I/O接口的状态,其声明如下:

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

select()函数中的参数nfds用来设置select监控的文件描述符的范围,需设置为文件描述符最大值加1。参数readfds、writefds、exceptfds分别用于表示可读取数据的文件描述符集、可写入数据的文件描述符集以及发生异常的文件描述符集,它们都为传入传出参数,其参数类型fd_set实质为长整型,这些集合中的每一位都对应一个文件描述符的状态,若集合参数被设置为NULL,表示不关心文件的对应状态。Linux系统中提供了一系列用于操作文件描述符集的函数,这些函数的定义与功能如表1所示。

表1 文件描述符集操作函数

函数声明 函数功能
void FD_CLR(int fd,fd_set *set); 将集合中的文件描述符fd清除(将fd位置为0)
int FD_ISSET(int fd,fd_set *set); 测试集合中文件描述符fd是否存在于集合中,若存在则返回非0
void FD_SET(int fd,fd_set *set); 将文件描述符fd添加到集合中(将fd位置为1)
void FD_ZERO(fd_set *set); 清除集合中所有的文件描述符(所有位置0)

参数timeout为struct timeval结构体类型的指针,该结构体的定义如下:

struct timeval{
  long tv_sec;
  long tv_user;
}

参数timeout用于设置select的阻塞时长,其取值有如下几种情况:

● 若timeval=NULL,表示永远等待;

● 若timeval>0,表示等待固定时长;

● 若timeval=0,select将在检查过指定文件描述符后立即返回(轮询)。

select()函数的返回值有3种:若返回值大于0,表示已就绪文件描述符的数量,此种情况下某些文件可读写或有错误信息;若返回值等于0,表示等待超时,没有可读写或错误的文件;若返回值-1,表示出错返回,同时errno将被设置。

select可监控的进程数量是有限的,该数量受到两个因素的限制。第一个因素是进程可打开的文件数量,第二个因素是select中的集合fd_set的容量。进程可打开文件的上限可通过ulimit –n命令或setrlimit函数设置,但系统所能打开的最大文件数也是有限的;select中集合fd_set的容量由宏FD_SETSIZE(定义在linux/posix_types.h中)指定,一般为1024,但即便通过重新编译内核的方式修改FD_SETSIZE,也不一定能提升select服务器的性能,因为若select一次监测的进程过多,单轮询便要耗费大量的时间。

下面通过一个案例来展示select()函数的用法,以及基于select模型的服务器的搭建方法。

案例3:使用select模型搭建多路I/O转接服务器,使服务器可接收客户端数据,并将接收到的数据转为大写,写回客户端;使客户端可向服务器发送数据,并将服务器返回的数据打印到终端。

案例实现如下:

select_s.c //服务器

 1    #include <stdio.h>
 2    #include <string.h>
 3    #include <stdlib.h>
 4    #include <netinet/in.h>
 5    #include <arpa/inet.h>
 6    #include "wrap.h"
 7    #define MAXLINE 80                        //设置监控的进程数上限
 8    #define SERV_PORT 8000
 9    int main()
 10    {
 11        int i, maxi, maxfd, listenfd, connfd, sockfd;
 12        int nready, client[FD_SETSIZE];     //FD_SETSIZE 默认为1024
 13        ssize_t n;
 14        fd_set rset, allset;
 15        char buf[MAXLINE];
 16        char str[INET_ADDRSTRLEN];            //#define INET_ADDRSTRLEN 16
 17        socklen_t cliaddr_len;
 18        struct sockaddr_in cliaddr, servaddr;
 19        listenfd = Socket(AF_INET, SOCK_STREAM, 0);
 20        bzero(&servaddr, sizeof(servaddr));
 21        servaddr.sin_family = AF_INET;
 22        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 23        servaddr.sin_port = htons(SERV_PORT);
 24        Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
 25        Listen(listenfd, 20);        //默认最大128
 26        maxfd = listenfd;
 27        maxi = -1;
 28         //初始化监控列表
 29        for (i = 0; i < FD_SETSIZE; i++)
 30            client[i] = -1;         //使用-1初始化client[]中元素
 31        FD_ZERO(&allset);
 32        FD_SET(listenfd, &allset);     //将listenfd添加到文件描述符集中
 33         //循环监测处于连接状态进程的文件描述符
 34        for (;;) {
 35            //使用变量rset获取文件描述符集合
 36            rset = allset;
 37             //记录就绪进程数量
 38            nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
 39            if (nready < 0)
 40                perr_exit("select error");
 41            if(FD_ISSET(listenfd,&rset)){//有新连接请求到达则进行连接便处理连接请求
 42                cliaddr_len = sizeof(cliaddr);
 43                connfd = Accept(listenfd, (struct sockaddr *)&cliaddr,
 44                                              &cliaddr_len);
 45                printf("received from %s at PORT %d\n",
 46                    inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
 47                    ntohs(cliaddr.sin_port));
 48                for (i = 0; i < FD_SETSIZE; i++)
 49                if (client[i] < 0) {
 50                    client[i] = connfd;     //将文件描述符connfd保存到client[]中
 51                    break;
 52                }
 53                if (i == FD_SETSIZE) {         //判断连接数是否已达上限
 54                    fputs("too many clients\n", stderr);
 55                    exit(1);
 56                }
 57                FD_SET(connfd, &allset);     //添加新文件描述符到监控信号集中
 58                if (connfd > maxfd)         //更新最大文件描述符
 59                    maxfd = connfd;
 60                if (i > maxi)                 //更新client[]最大下标值
 61                    maxi = i;
 62                 //若无文件描述符就绪,便返回select,继续阻塞监测剩余的文件描述符
 63                if (--nready == 0)
 64                    continue; 
 65            }
 66             //遍历文件描述符集,处理已就绪的文件描述符
 67            for (i = 0; i <= maxi; i++) {
 68                if ((sockfd = client[i]) < 0)
 69                    continue;
 70                if (FD_ISSET(sockfd, &rset)) {
 71                     //n=0,client就绪但未读到数据,表示client将关闭连接
 72                    if ((n = Read(sockfd, buf, MAXLINE)) == 0) {
 73                        //关闭服务器端连接
 74                        Close(sockfd);
 75                        FD_CLR(sockfd, &allset); //清除集合中对应的文件描述符
 76                        client[i] = -1;
 77                    }
 78                    else {                        //处理获取的数据
 79                        int j;
 80                        for (j = 0; j < n; j++)
 81                            buf[j] = toupper(buf[j]);
 82                        Write(sockfd, buf, n);
 83                    }
 84                    if (--nready == 0)
 85                        break;
 86                }
 87            }
 88        }
 89        close(listenfd);
 90        return 0;
 91    }

select_c.c //客户端

 1    #include <stdio.h>
 2    #include <string.h>
 3    #include <unistd.h>
 4    #include <netinet/in.h>
 5    #include "wrap.h"
 6    #define MAXLINE 80
 7    #define SERV_PORT 8000
 8    int main()
 9    {
 10        struct sockaddr_in servaddr;
 11        char buf[MAXLINE];
 12        int sockfd, n;
 13        sockfd = Socket(AF_INET, SOCK_STREAM, 0);
 14        bzero(&servaddr, sizeof(servaddr));
 15        servaddr.sin_family = AF_INET;
 16        inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
 17        servaddr.sin_port = htons(SERV_PORT);
 18        Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
 19        while (fgets(buf, MAXLINE, stdin) != NULL) {
 20            Write(sockfd, buf, strlen(buf));
 21            n = Read(sockfd, buf, MAXLINE);
 22            if (n == 0)
 23                printf("the other side has been closed.\n");
 24            else
 25                Write(STDOUT_FILENO, buf, n);
 26        }
 27        Close(sockfd);
 28        return 0;
 29    }

分别使用以下语句编译服务器端程序与客户端程序:

gcc select_s.c wrap.c -o server
gcc select_c.c wrap.c -o client

程序编译完成后,先执行服务器程序,打开服务器,之后在一个终端运行客户端程序(记为客户端1),并在该终端中输入客户端需要发送的数据,此时客户端与服务器端中打印的信息分别如下:

客户端1:

hello
HELLO

服务器端:

received from 127.0.0.1 at PORT 60315

打开新的终端,在该终端中再次运行客户端程序(记为客户端2),并输入要发送的数据,此时终端2与服务器端中打印的信息分别如下:

客户端2:

itheima
ITHEIMA

服务器端:

received from 127.0.0.1 at PORT 41897
received from 127.0.0.1 at PORT 41898

由程序执行结果可知,案例3实现成功。

点击此处
隐藏目录