C++ Select()

发布时间:2023-05-18

C++ Select()是一种系统调用函数,能在一组文件描述符中强制时间限制内等待一个或多个条件成立,可用于I/O多路复用。下面从不同方面对C++ Select()做详细的阐述。

一、C Select函数

C Select函数的原型为:

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

其中:

  • nfds为文件描述符集合中数值最大的描述符加1
  • readfdswritefdsexceptfds为文件描述符集合
  • timeout表示阻塞时间 C Select函数用于检查文件描述符集合中是否有可读、可写、异常等事件,可用于单进程内多个文件描述符的I/O事件监听。 以下为C++ Select函数使用示例:
#include<stdio.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
int main() {  
    fd_set rfds;  
    struct timeval tv;  
    int retval;  
    char buf[256];  
    /* 等待stdin(标准输入)上的输入 */  
    FD_ZERO(&rfds);  
    FD_SET(STDIN_FILENO, &rfds);  
    /* 等待5秒钟 */  
    tv.tv_sec = 5;  
    tv.tv_usec = 0;  
    retval = select(1, &rfds, NULL, NULL, &tv);  
    /* 由于我们只等待stdin上的输入,所以不需要传递参数除STDIN_FILENO外 */
    if (retval == -1)  
        perror("select()");  
    else if (retval) {  
        fgets(buf, sizeof buf, stdin);  
        printf("输入: %s\n", buf);  
    }  
    else  
        printf("没有输入数据在五秒内.\n");  
    return 0;  
}

二、C Selection和S Selection

C Selection和S Selection都是Select函数的改进版,其扩展了参数类型,可以通过hash表实现高效的查找,提升了系统利用率。其中,C Selection适用于用户空间,S Selection适用于内核空间,可以用于高速网络和操作系统的性能优化。 C++ Select函数的相关参数和C Selection、S Selection相比,基本一致。在使用S Selection时,需要在建立socket时进行一个参数的设置,同时需要用到poll函数。 下面是借用《UNIX环境高级编程》一书中关于S Selection的代码示例:

#include<sys/time.h>
#include<sys/types.h>
/* Selserv.cpp */
int
main(int argc, char *argv[])
{
    int    i, maxi, maxfd, listenfd, connfd, sockfd;
    int    nready, client[FD_SETSIZE];
    ssize_t    n;
    fd_set rset, allset;
    char   buf[MAXLINE];
    char   str[INET_ADDRSTRLEN];
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;
    if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        err_sys("socket error");
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(SERV_PORT);
    if (bind(listenfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
        err_sys("bind error");
    if (listen(listenfd, LISTENQ) < 0)
        err_sys("listen error");
    else
        printf("<时间%ld>  监听端口 %d on %s\n",
               time(NULL), SERV_PORT, inet_ntop(AF_INET, &servaddr.sin_addr, str, sizeof(str)));
    maxfd = listenfd;        /* 定义初始最大描述符值 */
    maxi = -1;                /* 有效客户连接下标 */
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;        /* 初始化 */
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset); /* 添加监听描述符 */
    for ( ; ; ) {
        rset = allset;        /* 每次循环时都重新设置select的可读文件描述符集 */
        if ( (nready = select(maxfd+1, &rset, NULL, NULL, NULL)) < 0)
            err_sys("select error");
        if (FD_ISSET(listenfd, &rset)) { /* 新的客户连接请求 */
            clilen = sizeof(cliaddr);
            if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0)
                err_sys("accept error");
            printf("<时间%ld>:新的客户连接来自%s: %d\n",
                   time(NULL), 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;    /* 保存新的描述符 */
                    break;
                }
            if (i == FD_SETSIZE)
                err_quit("too many clients");
            FD_SET(connfd, &allset);    /* 添加新的文件描述符到读集合中 */
            if (connfd > maxfd)
                maxfd = connfd;            /* for select */
            if (i > maxi)
                maxi = i;                /* 数组client中最大元素的下标 */
            if (--nready <= 0)
                continue;            /* 没有更多就绪事件时,继续回到select()调用*/
        }
        for (i = 0; i <= maxi; i++) {    /* 检测客户端socket */
            if ( (sockfd = client[i]) < 0)
                continue;
            if (FD_ISSET(sockfd, &rset)) {
                if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
                    /* 当客户关闭连接时,服务器也关闭对应的连接,并将相应的client数组元素设置为-1,表示空闲 */
                    close(sockfd);
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                } else {
                    Writen(sockfd, buf, n);
                }
                if (--nready <= 0)
                    break;                /* 没有更多就绪事件时,继续回到select()调用*/
            }
        }
    }
}

三、C Select语句

C Select语句是select函数的一种可移植、跨平台的I/O多路复用机制,可以在一组文件描述符上等待多个I/O事件,多线程同时处理。C Select语句适用于网络编程中,方便实现高并发的服务器。 以下是一个使用C Select语句进行I/O复用的示例:

/* 实现一个TCP服务器,监听10086端口,并在有客户端连接时返回当前系统时间 */
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/select.h>
#include<time.h>
const int MAXLINE = 1024, LISTENQ = 5, SERV_PORT = 10086;
int main() {
    int i, maxi, maxfd, listenfd, connfd, sockfd;
    int nready, client[FD_SETSIZE];
    ssize_t n;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;
    fd_set rset, allset;
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("error: socket");
        return -1;
    }
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
        perror("error: bind");
        return -1;
    }
    if (listen(listenfd, LISTENQ) == -1) {
        perror("error: listen");
        return -1;
    }
    maxfd = listenfd;
    maxi = -1;
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);
    for (;;) {
        rset = allset;
        if ((nready = select(maxfd + 1, &rset, NULL, NULL, NULL)) == -1) {
            perror("error: select");
            return -1;
        }
        if (FD_ISSET(listenfd, &rset)) {
            clilen = sizeof(cliaddr);
            connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
            printf("accept client ip=%s 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;
                    break;
                }
            if(i == FD_SETSIZE) {
                perror("too many clients");
                return -1;
            }
            FD_SET(connfd, &allset);
            if (connfd > maxfd)
                maxfd = connfd;
            if (i > maxi)
                maxi = i;
            if (--nready == 0)
                continue;
        }
        for (i = 0; i <= maxi; i++) {
            if ((sockfd = client[i]) < 0)
                continue;
            if (FD_ISSET(sockfd, &rset)) {
                if ((n = read(sockfd, buf, MAXLINE)) == 0) {
                    close(sockfd);
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                }
                else {
                    time_t t = time(NULL);
                    char buffer[1024] = {0};
                    strftime(buffer, 1024, "%Y-%m-%d %H:%M:%S\n", localtime(&t));
                    write(sockfd, buffer, strlen(buffer));
                }
                if (--nready == 0)
                    break;
            }
        }
    }
    return 0;
}

四、C Select详解与模型

C Select是一种基于事件驱动的编程范式,其主要用于网络编程中的I/O多路复用与高并发服务器。其基本工作原理为:首先需要定义一组文件描述符,通过select函数等待其事件到达,当其中任意一个文件描述符事件发生变化时,select函数返回;在返回后,通过遍历文件描述符集合获取到其中发生变化的文件描述符并进行对应处理。 C Select模型与并发服务器中的运作模型是有关联的:在并发服务器中,通过创建多线程来支持多并发客户端连接,当每个线程需要处理多个不同的客户端请求时,其I/O事件规律拓扑结构应采用Multiplexer或Reactor这两种I/O通信模型,其中,Multiplexer模型使用了Select程序,而Reactor模型使用的是相对更为高级的Epoll机制。 以下为一个简单的使用C Select函数的示例:

#include<stdio.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/time.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
void server(int port) {
  int fd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
  if (fd == -1) {
    perror("socket");
    return;
  }
  int optval = 1;
  setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval); // 配置选项
  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = htonl(INADDR_ANY);
  addr.sin_port = htons(port);
  if (bind(fd, (struct sockaddr*)&addr, sizeof addr) == -1) { // 绑定端口