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能够如期地工作。

 

posted @ 2016-04-26 10:08  LarryKnight  阅读(286)  评论(0编辑  收藏  举报