C++网络编程(九)select模型(含服务端代码示例)
Select I/O 复用模型详解
在计算机网络编程中,高效处理多个 I/O 操作是关键挑战之一。Select 是一种经典的 I/O 复用技术,它允许单个进程同时监控多个文件描述符(如套接字),从而避免为每个连接创建单独线程或进程的开销。本文将深入解析 Select 模型的主旨思想、工作原理、函数参数及使用细节,帮助您全面理解这一机制。
Select 的主旨思想
Select 的核心思想基于以下三点:
-
构建文件描述符列表:首先,程序需要创建一个包含所有待监听文件描述符的集合。这些描述符可能包括监听套接字(用于接受新连接)和通信套接字(用于数据传输)。
-
内核阻塞检测:通过调用 Select 函数,将这个集合传递给内核。内核会阻塞(即等待)直到集合中至少有一个文件描述符准备好进行 I/O 操作(如可读、可写或出现异常)。
-
状态通知返回:当有描述符就绪时,Select 函数返回,并通知进程哪些描述符发生了变化。进程随后可以针对这些描述符执行相应的 I/O 操作,而无需盲目轮询。
这种方法实现了高效的 I/O 多路复用,减少了资源消耗,尤其适用于高并发场景,如服务器处理多个客户端连接。
Select 的工作原理
Select 的工作原理可以分解为几个关键步骤,确保跨平台兼容性(支持 Linux 和 Windows)和高效检测。以下是详细过程:
-
跨平台函数调用:Select 是一个标准函数,在不同操作系统上通过底层系统 API 实现。当调用 Select 时,它委托操作系统内核执行实际的检测操作。
-
参数传递与集合定义:在调用 Select 时,进程通过参数传递三个文件描述符集合:
-
读集合(readfds):检测描述符的读缓冲区状态。常见用途包括:
-
监听描述符:检查是否有新客户端连接请求。
-
通信描述符:检查是否有数据到达可读。
-
-
写集合(writefds):检测描述符的写缓冲区是否可写(即有空间写入数据)。但由于写缓冲区通常可用,这个集合较少使用。
-
异常集合(exceptfds):检测描述符在读写操作中是否发生错误(如连接中断)。
这些集合都是传入传出参数:进程传入初始集合,内核检测后修改集合内容并返回,仅包含就绪的描述符。
-
-
内核检测机制:内核接收到集合后,采用线性扫描算法遍历文件描述符表。它会检查每个描述符是否满足条件:
-
对于读集合:读缓冲区中有数据可读。
-
对于写集合:写缓冲区可写。
-
对于异常集合:出现读写错误。
内核遍历的范围由
nfds参数控制(最大描述符值加一),确保高效扫描。
-
-
状态反馈与返回:检测完成后,内核修改传入的集合,只保留就绪的描述符。Select 函数 then returns with the number of ready descriptors or an error code. 进程可以通过检查返回的集合来确定具体哪些描述符需要处理。
Select 函数原型与参数详解
Select 的函数原型如下(基于 C 语言):
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解释:
-
nfds:指定检测的文件描述符范围,值为所有集合中最大文件描述符值加一(例如,如果最大描述符为 12,则 nfds=13)。这是因为内核使用线性遍历,nfds 作为遍历上限,提高效率。
-
readfds:指向读集合的指针。常用且重要,用于检测可读事件。是传入传出参数:
-
传入:委托内核检测的描述符集合。
-
传出:内核返回就绪的描述符集合。传出元素数量总是小于或等于传入数量。
-
-
writefds:指向写集合的指针。较少使用,因为写缓冲区通常可写;如果不需要检测,可设为 NULL。同样是传入传出参数。
-
exceptfds:指向异常集合的指针。用于检测错误情况,如连接异常;通常设为 NULL 除非需要错误处理。也是传入传出参数。
-
timeout:指定超时时间结构体指针。控制 Select 的阻塞行为:
-
如果指向一个
struct timeval(包含秒和微秒),Select 会在超时后返回,即使没有就绪描述符。 -
如果设为 NULL,Select 会无限阻塞直到有描述符就绪。
-
如果设为 0(即指向一个零超时结构),Select 立即返回,用于非阻塞轮询。
-
struct timeval定义:
struct timeval {
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
};
返回值:
-
大于 0:返回就绪描述符的总数(所有集合的总和)。
-
等于 0:超时时间内没有描述符就绪,函数返回。
-
-1:调用失败,可能由于错误如无效参数;应检查 errno 获取详情。
使用注意事项与总结
Select 模型的主要优点包括跨平台支持和简单性,但它也有局限性:
-
线性扫描效率低:当文件描述符数量很大时(如成千上万),内核的线性遍历会成为性能瓶颈,因为时间复杂度为 O(n)。
-
集合大小限制:文件描述符集合有固定大小(通常由 FD_SETSIZE 定义),可能限制可监控的描述符数量。
-
重复初始化:由于集合是传入传出参数,每次调用 Select 前都需要重新初始化集合,增加了编程复杂度。
服务端代码示例
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<sys/select.h>
int main ()
{
//1.创建服务器插座
int lfd=socket(AF_INET,SOCK_STREAM,0);
if (lfd==-1)
{
perror("socket创建失败\n");
exit(0);
}
//2.绑定
struct sockaddr_in addr;
addr.sin_family=AF_INET;//ipv4
addr.sin_port=htons(9999);//端口,转为网络字节序
addr.sin_addr.s_addr=INADDR_ANY;//0地址
int ret=bind(lfd,(struct sockaddr*)&addr,sizeof(addr));
if (ret==-1)
{
perror("绑定失败\n");
exit(0);
}
//3.设置监听
ret=listen(lfd,128);
if (ret==-1)
{
perror("监听失败\n");
exit(0);
}
//4.初始化检测的集合
fd_set reads,tmp; //reads为读集合,tmp是备份
FD_ZERO(&reads); //清0
FD_SET(lfd,&reads); //添加监听文件描述符,也就是原本accept阻塞等待的那个
int nfds=lfd;
//5.不停地委托内核检测集合中的文件描述符状态
while(1)
{
tmp = reads; //防止select修改本尊,因为第二参数是个传出参数
//tmp是fd_set型,即1024位,每一位对应内核中的一个文件描述符
//如果一个文件描述符的读缓存区有数据,那么在tmp中对应的那一位将从0->1,表示有数据了
int num = select(nfds+1,&tmp,NULL,NULL,NULL);
printf("num = %d\n",num);
for (int i=0;i<=nfds;i++) //遍历文件描述符表,查看各个文件描述符是否可用
{
//该函数可判断一个文件描述符在fd_set的对应位置是否存在,也就是那一位是否为1
if(FD_ISSET(lfd,&tmp)&&i==lfd) //lfd在tmp存在,说明lfd的可读缓存区中有数据,说明有新的连接
{
//建立新连接,这次调用绝对不阻塞
int cfd= accept(lfd,NULL,NULL); //不需要客户端信息,后两个参数写NULL
//得到了与客户端通信的文件描述符,将其添加到检测的集合中
FD_SET(cfd,&reads);
//更新最大的nfds
nfds = nfds < cfd ? cfd:nfds;
}
else
{
//剩下的都是用于通信文件描述符
if(FD_ISSET(i,&tmp))
{
//直接读数据,肯定不阻塞
char buf[1024];
memset(buf,0,sizeof(buf));
int len=recv(i,buf,sizeof(buf),0);//没数据时阻塞在这里
//判断读写情况
if (len==0)
{
printf("客户端已断开连接\n");
//将该文件描述符从检测集合中删除
FD_CLR(i,&reads);
close(i);
}else if(len>0)
{
printf ("recv buf: %s\n",buf);
//回复数据
send(i,buf,strlen(buf)+1,0);//+1是为了带上\0
}
else
{
perror("接受数据失败\n");
break;
}
}
}
}
}
//6.断开连接
close(lfd);//监听编号
return 0;
}
在实际应用中,Select 适用于中小规模并发场景。对于更高性能需求,可以考虑更现代的替代方案如 epoll(Linux)或 kqueue(BSD)。
更多推荐


所有评论(0)