select()
select(),用于确定一个或多个套接口的状态,对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息,用fd_set结构来表示一组等待检查的套接口,在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select()返回满足条件的套接口的数目。
该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段时间后才唤醒它。
#include <sys/select.h>
int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
maxfdpl: 是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1。
readset: (可选)指针,指向一组等待可读性检查的套接口。
writeset: (可选)指针,指向一组等待可写性检查的套接口。
exceptset:(可选)指针,指向一组等待错误检查的套接口。
timeout: select()最多等待时间,对阻塞操作则为NULL。
函数返回:若有就绪描述符则为其数目;超时则为0;出错则为-1(如捕获到中断信号)。
struct timeval{
long tv_sec; // seconds
long tv_usec; // microseconds
};
timeval参数用于指定这段时间的秒数和微秒数,此参数有三种可能:
1)永远等待: 设为空指针,仅在有一个描述符准备好I/O时才返回。
2)等待一段固定的时间:设置一定的时间,在该时间内有一个描述符准好I/O时返回。
3)不等待: 设置为0,检测描述符后立即返回(轮询polling)。
前两种情况的等待通常会被进程在等待期间捕获的信号中断,并从信号处理函数返回。
若将select()中间的三个参数都设置为空,则其为比sleep函数更为精确的定时器(sleep睡眠以秒为最小单位),poll函数也提供类似功能,用的是sleep_us函数(以微秒为单位)。
使用select时最常见的两个编程错误是:忘记对最大描述符加1;忘记描述符集是值-结果参数。
使用select对str_cli函数进行重写,这样服务器一终止,客户就能马上得到通知。
早先版本的问题在于:当套接字上发生某些事件时,客户可能阻塞于fgets调用。新版本改为阻塞于select调用,或是等待标准输入可读,或是等待套接字可读。
下图为str_cli函数中由select处理的各种条件。

客户的套接字上的三个条件处理:
1)若对端TCP发送数据,套接字变为可读,并read返回一个大于0的值(即读入数据的字节数);
2)若对端TCP发送FIN(对端进程终止),套接字变为可读,并read返回0(EOF);
3)若对端TCP发送RST(对端主机崩溃并重新启动),套接字变为可读,并read返回-1,errno中含有确切的错误码。
str_cli函数修订版
void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; int maxfdpl; int filefd = fileno(fp); // int fileno(FILE *stream)用于获取文件流所使用的文件描述符(把标准I/O文件指针转换为对应的描述符) fd_set rset; FD_ZERO(&rset); // 初始化用于检查可读性的描述符集 for (; ;){ FD_SET(filefd, &rset); FD_SET(sockfd, &rset); // 将描述符加入rset集合 maxfdpl = max(filefd, sockfd) + 1; select(maxfdpl, &rset, NULL, NULL, NULL); // 读集合指针非空,写和异常集合指针及时间参数均为空指针,该调用阻塞到某个描述符就绪为止。 if (FD_ISSET(sockfd, &rset)){ // 检测描述符是否在rset集合中 if (read(sockfd, recvline, MAXLINE) == 0){ cout<<"str_cli:server terminated prematurely!"<<endl; exit(0); } fputs(recvline, stdout); bzero(recvline, sizeof(recvline); } if (FD_ISSET(filefd, &rset)){ if (fgets(sendline, MAXLINE, fp) == NULL) return; write(sockfd, sendline, strlen(sendline)); } } }
终止网络连接通常是调用close函数,但close有两个限制,此时shutdown函数可避免:
1)close把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。使用shutdown可以不管引用计数就激发TCP的正常连接终止;
2)close终止读和写两个方向的数据传送。
调用shutdown关闭一半TCP连接:

#include <sys/socket.h>
int shutdown(int sockfd, int howto); // 成功返回0;出错返回-1
howto参数:
1)SHUT_RD:关闭连接的读这一半。套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。
2)SHUT_WR:关闭连接的写这一半。对于TCP套接字称为半关闭(half-close)。当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。
3)SHUT_RDWR:连接的读半部和写半部都关闭。
str_cli函数再修订版
void str_cli(FILE *fp, int sockfd) { int maxfdpl, stdineof; int filefd = fileno(fp); // int fileno(FILE *stream)用于获取文件流所使用的文件描述符(把标准I/O文件指针转换为对应的描述符) fd_set rset; int n; char buf[MAXLINE]; stdineof = 0; // 初始化为0的新标准 FD_ZERO(&rset); // 初始化用于检查可读性的描述符集 for (; ;){ if (0 == stdineof) // 只要该标志为0,每次在循环中总是select标准输入的可读性 FD_SET(filefd, &rset); FD_SET(sockfd, &rset); // 将描述符加入rset集合 maxfdpl = max(filefd, sockfd) + 1; select(maxfdpl, &rset, NULL, NULL, NULL); // 读集合指针非空,写和异常集合指针及时间参数均为空指针,该调用阻塞到某个描述符就绪为止。 if (FD_ISSET(sockfd, &rset)){ // socket is readable if ( (n = read(sockfd, buf, MAXLINE)) == 0){ // 当在套接字读到EOF时,如果已在标准输入上遇到EOF,那就是正常终止;如果在标准输入上未遇到EOF,那么服务器进程已过早终止。 if (1 == stdineof) return; // normal termination else{ cout<<"str_cli:server terminated prematurely!"<<endl; exit(0); } } write(fileno(stdout), buf, n); } if (FD_ISSET(filefd, &rset)){ // input is readable // 当在标准输入上碰到EOF时,将新标志置1,并向sockfd发送FIN if ( (n = read(filefd, buf, MAXLINE) == 0){ stdineof = 1; shutdown(sockfd, SHUT_WR); // send FIN FD_CLR(filefd, &rset); continue; } write(sockfd, buf, n); } } }
函数中改用read和write对缓冲区而不是文本行进行操作,使得select能够如期地工作。