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实现成功。