【计算机网络】IO复用方法(二)——Select
目录
一、select 方法概述
二、select 系统调用原型
三、select 的基本原理
四、使用 select 的步骤
五、将select应用到tcp服务端
六、select 的优缺点
一、select 方法概述
select 是 Unix/Linux 系统中一种经典的 I/O 多路复用技术,允许程序监视多个文件描述符(如套接字、管道等),直到其中一个或多个描述符准备好进行 I/O 操作(如可读、可写或发生异常)。select 适用于需要同时处理多个 I/O 通道的场景,如网络服务器、客户端等。
二、select 系统调用原型
#include <sys/select.h>
int select( int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,struct timeval* timeout);
参数设置:
1、nfds参数:指定被监听的文件描述符的总数。它通常被设置为select 监听的所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的。
2、readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通过这3个参数传入自己感兴趣的文件描述符。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。一般只关注读,后面两个参数设置为空指针。这三个参数是fd_set结构指针类型。
3、timeout:代表超时时间;设为 NULL 表示无限阻塞,设为 0 表示非阻塞轮询。
三、select 的基本原理
select 通过三个文件描述符集合(fd_set)来监视不同类型的 I/O 事件:
- readfds:监视描述符是否可读(如数据到达、连接关闭)。
- writefds:监视描述符是否可写(如发送缓冲区空闲)。
- exceptfds:监视描述符是否发生异常(如带外数据到达)。
调用 select 时,内核会阻塞进程,直到至少一个描述符就绪或超时。返回后,内核会修改这些集合,仅保留就绪的描述符。
给select传入一个集合,会返回告诉这个集合中有多少个就绪,集合中就绪对应的位会被设置成1,没有就绪就会设置成0。最多可以检测1024给描述符。同时select为周期性的调用。

四、使用 select 的步骤
int main(){int fd=STDIN;//输入fd_set fdset;while (1)//把描述符添加到集合中{FD_ZERO(&fdset);//清空集合FD_SET(fd,&fdset);//将描述符添加到集合里struct timeval tv={5,0};//设置超时时间,时间每次重置int n=select(fd+1,&fdset,NULL,NULL,&tv);//阻塞,最多阻塞5sif(n==-1){printf("select err\n");}else if(n==0){//超时printf("time out\n");}else{if(FD_ISSET(fd,&fdset))//真,fd有读事件发生{char buff[128]={0};read(fd,buff,127);printf("read:%s\n",buff);}}
}
初始化描述符集合: 使用 FD_ZERO 清空集合,FD_SET 添加需要监视的描述符。
调用 select 传入初始化后的集合和超时时间。select 会阻塞直到事件就绪或超时。
检查返回值
- 返回 -1 表示错误(如被信号中断)。
- 返回 0 表示超时。
- 返回正数表示就绪的描述符数量。
处理就绪的描述符 使用 FD_ISSET 检查哪些描述符就绪,并执行相应操作
五、将select应用到tcp服务端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#define MAXFD 10int socket_init();
void fds_init(int fds[]){//初始化数组for(int i = 0; i < MAXFD; i++){fds[i] = -1;}
}
//添加
void fds_add(int fds[], int fd){for(int i = 0; i < MAXFD; i++ ){if( fds[i] == -1){fds[i] = fd;break;//当找到一个-1,就停止}}
}
//移除
void fds_del(int fds[], int fd){for(int i = 0; i < MAXFD; i++){if( fds[i] == fd){fds[i] = -1;break;}}
}
int main(){int sockfd = socket_init();//套接字if( sockfd == -1){exit(1);}int fds[MAXFD];//数组fds_init(fds);//-1,初始化数组,为空fds_add(fds,sockfd);//添加描述符fd_set fdset;//selectwhile( 1 ){FD_ZERO(&fdset);//清空int maxfd = -1;//将所有描述符添加到集合中for(int i = 0; i < MAXFD; i++){if( fds[i] == -1){//找到有效的continue;}FD_SET(fds[i],&fdset);//添加if( maxfd < fds[i]){//找到最大的描述符maxfd = fds[i];}}struct timeval tv = {5,0};//超时时间int n = select(maxfd+1,&fdset,NULL,NULL,&tv);//阻塞,5sif( n == -1){printf("select err\n");}else if( n == 0 ){//超时printf("time out\n");}else{//有n个描述符就绪,遍历整个数组找到有效的for(int i = 0; i < MAXFD; i++){if( fds[i] == -1){//无效描述符continue;}if( FD_ISSET(fds[i],&fdset)){//真,有事件发生,检测有无事件发生//判断套接字类型if( fds[i] == sockfd ){//监听套接字 acceptstruct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);if( c < 0 ){continue;}printf("accept c=%d\n",c);//接受链接fds_add(fds,c); //把c加入到数组中,/*因为等待该循环将事件监测完毕,进入下一轮循环时,又重新清空集合,将数组的所有元素重新添加至集合,此时c也被加入到集合中,再次进行select检测,下一轮就有两个描述符以供检测*/}else{//连接套接字有数据 recvchar buff[128] = {0};int num = recv(fds[i],buff,127,0);if( num <= 0 ){close(fds[i]);fds_del(fds,fds[i]);printf("close\n");}else{printf("recv:%s\n",buff);send(fds[i],"ok",2,0);}}}}}}
}
int socket_init(){//创建监听套接字int sockfd = socket(AF_INET,SOCK_STREAM,0);if( sockfd == -1){//创建套接字失败return -1;}struct sockaddr_in saddr;//ipv4专用,制定ip端口memset(&saddr,0,sizeof(saddr));//清空saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定,通用套接字if( res == -1){printf("bind err\n");return -1;}res = listen(sockfd,5);//监听队列if( res == -1){return -1;}return sockfd;
}
客户端:

服务端:

六、select 的优缺点
优点
-
跨平台支持,几乎在所有 Unix-like 系统上可用。
-
实现简单,适合少量连接或对性能要求不高的场景。
缺点
-
文件描述符数量受限(通常为 1024),高并发场景性能较差。
-
每次调用需重新传入描述符集合,内核和用户空间频繁拷贝数据。
-
线性扫描所有描述符,效率随描述符数量增加而下降。
