多路IO转接(一):select

    技术2022-07-11  119

    一、select函数说明

    原理: 借助内核, select 来监听客户端连接、数据通信事件。 函数原型: int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); 其中 fd_set 实际上是一个位图,每一个特定二进制位标志相应文件描述符 nfds:监听的所有文件描述符中,最大文件描述符+1 readfds: 读 文件描述符监听集合。 传入、传出参数 writefds:写 文件描述符监听集合。 传入、传出参数 exceptfds:异常 文件描述符监听集合 传入、传出参数 timeout: > 0: 设置监听超时时长。 NULL: 阻塞监听 0: 非阻塞监听,轮询 返回值: > 0: 所有监听集合(3个)中, 满足对应事件的总数。 0: 没有满足监听条件的文件描述符 -1: 出错 fd_set参数的相关辅助函数: void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。 void FD_SET(int fd, fd_set *set); --- 将待监听的文件描述符,添加到监听集合中 void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。 int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。 FD_ISSET返回值: 1)文件描述符在监听集合中:返回1; 2)文件描述符不在监听集合中:返回0;

    二、select特点

    它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

    优点

    跨平台。win、linux、macOS、Unix、类Unix、mips等操作系统均可使用。

    缺点

    select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

    1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

    一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

    2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

    当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

    3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大 参考链接 select、poll、epoll之间的区别(搜狗面试)

    三、demo

    #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <ctype.h> #include <sys/select.h> #include "wrap.h" #define SERV_PORT 6666 int main(int argc, char *argv[]) { int i, j, n, maxi; int nready, client[FD_SETSIZE]; /* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */ int maxfd, listenfd, connfd, sockfd; char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */ struct sockaddr_in clie_addr, serv_addr; socklen_t clie_addr_len; fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */ listenfd = Socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family= AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port= htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); Listen(listenfd, 128); maxfd = listenfd; /* 起初 listenfd 即为最大文件描述符 */ maxi = -1; /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */ for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; /* 用-1初始化client[] */ FD_ZERO(&allset); FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */ while (1) { rset = allset; /* 每次循环时都从新设置select监控信号集 */ nready = select(maxfd+1, &rset, NULL, NULL, NULL); //2 1--lfd 1--connfd if (nready < 0) perr_exit("select error"); if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */ clie_addr_len = sizeof(clie_addr); connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */ printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)), ntohs(clie_addr.sin_port)); for (i = 0; i < FD_SETSIZE; i++) if (client[i] < 0) { /* 找client[]中没有使用的位置 */ client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */ break; } if (i == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */ fputs("too many clients\n", stderr); exit(1); } FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */ if (connfd > maxfd) maxfd = connfd; /* select第一个参数需要 */ if (i > maxi) maxi = i; /* 保证maxi存的总是client[]最后一个元素下标 */ if (--nready == 0) continue; } for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */ if ((sockfd = client[i]) < 0) continue; if (FD_ISSET(sockfd, &rset)) { if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */ Close(sockfd); FD_CLR(sockfd, &allset); /* 解除select对此文件描述符的监控 */ client[i] = -1; } else if (n > 0) { for (j = 0; j < n; j++) buf[j] = toupper(buf[j]); Write(sockfd, buf, n); Write(STDOUT_FILENO, buf, n); } if (--nready == 0) break; /* 跳出for, 但还在while中 */ } } } Close(listenfd); return 0; }
    Processed: 0.016, SQL: 10