/* According to POSIX.1-2001, POSIX.1-2008 */
#include
/* According to earlier standards */
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指 的时间后才唤醒它然后返回。
作为一个例子,我们可以调用select,告知内核仅在下列情况发生时才返回:
也就是说,我们调用select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等
待多长时间,当感兴趣的事件到来时或经历10.2秒之后,则返回。
注意:我们感兴趣的描述符不局限于套接字,任何描述符都可以使用select来测试。
nfds:
指定待测试的描述符个数,它的值是待测试的最大描述符 +1(因为描述符是从0开始的),表示描述符0、1、2...maxfds-1均将被测试。
注意:该值最大不能超过1024,如果超过1024则改用poll或epoll代替select
readfds、writefds、exceptfds:
中间的三个参数都是传入传出参数,指定我们要让内核测试读、写和异常条件的描述符。
如果对某个条件不感兴趣,就可以把它设为NULL。struct fd_set可以理解为一个集合,
这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
void FD_ZERO(fd_set *fdset); //清空集合,类似于string::clear()
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中,类似于insert()
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除,类似于earse()
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写,类似于find()
timeout:
告知内核等待所指定描述字中的任何一个就绪可花多少时间。
其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
这个参数有三种可能:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须全为0。
注:前两种情形的等待通常会被进程在等待期间捕获的信号中断,并从信号处理函数返回。
返回值:
表示跨所有描述符集的已就绪的总位数。如果在任何描述符就绪之前定时器到时,那么返回0。
返回-1表示出错(这是可能发生的,譬如本函数被一个所捕获的信号中断)。
头文件
中定义的FD_ SETSIZE
常值是数据类型fd_ set
中的描述符总数,其值通常是1024
,不过很少有程序用到那么多的描述符。nfds
参数迫使我们计算出所关心的最大描述符并告知内核该值。以前面给出的打开描述符1、4和5的代码为例,其nfds
值就是6而不是5的原因在于:我们指定的是描述符的个数而非最大值,而描述符是从0开始的。
select
函数通过修改由指针readset、writeset和exceptset
所指向的的描述符集,来修改对应额值,因而这三个参数都是值一结果参数。调用该函数时,我们指定所关心的描述符的值设为1,该函数返回时,结果将指示哪些描述符已就绪。该函数返回后,我们使用FD_ ISSET
宏来测试fd_ set
数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清成0
,为此,每次重新调用select
函数时,我们都得再次把所有描述符集内所关心的位均置为1
。
使用select时最常见的两个编程错误是:
第二个错误导致调用select时,描述符集内我们认为是1的位却被置为0。
/* server.c */
#include
#include
#include
#include
#include
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE]; /* FD_SETSIZE 默认为 1024 */
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
socklen_t cliaddr_len;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20); /* 默认最大128 */
maxfd = listenfd; /* 初始化 */
maxi = -1; /* client[]的下标 */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* 用-1初始化client[] */
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
for ( ; ; ) {
rset = allset; /* 每次循环时都从新设置select监控信号集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* new client connection */
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < FD_SETSIZE; i++) {
if (client[i] < 0) {
client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
break;
}
}
/* 达到select能监控的文件个数上限 1024 */
if (i == FD_SETSIZE) {
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); /* 添加一个新的文件描述符到监控信号集里 */
if (connfd > maxfd)
maxfd = connfd; /* select第一个参数需要 */
if (i > maxi)
maxi = i; /* 更新client[]最大下标值 */
if (--nready == 0)
continue; /* 如果没有更多的就绪文件描述符继续回到上面select阻塞监听,
负责处理未处理完的就绪文件描述符 */
}
for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */
if ( (sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
Close(sockfd); /* 当client关闭链接时,服务器端也关闭对应链接 */
FD_CLR(sockfd, &allset); /* 解除select监控此文件描述符 */
client[i] = -1;
} else {
int j;
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
}
if (--nready == 0)
break;
}
}
}
close(listenfd);
return 0;
}
/* client.c */
#include
#include
#include
#include
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
解决1024
以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,此时可考虑用poll
和epoll
代替select
(1) 满足下列四个条件中的任何一个时,一个套接字准备好读。
SO_RCVLOWAT
套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值为1。accept
通常不会阻塞。errno
设置成确切的错误条件。这些待处理错误( pending error)也可以通过指定SO_ERROR
套接字选项调用getsockopt
获取并清除。(2)下列四个条件中的任何一个满足时,一个套接字准备好写。
SO_ SNDLOWAT
套接字选项来设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值通常为2048。SIGPIPE
信号。errno
设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ ERROR
套接字选项调用getsockopt
获取并清除。(3)如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。
图6-7汇总了.上述导致select返回某个套接字就绪的条件。
上篇:Unix下可用的5种I/O模型总结
下篇:IO复用之poll函数介绍