select函数概述
select
函数是一种用于实现I/O复用的方法,它可以让程序在多个文件描述符(例如套接字)之间进行选择,以便在其中任何一个或多个可用时执行I/O操作。这种机制使得程序能够更高效地处理多个I/O操作。下面将对select
的原理和工作机制进行详细介绍,并分析select
函数的优势和局限。
1. select的原理和工作机制
select
函数的原理和工作机制可以概括为以下几个步骤:
-
初始化文件描述符集合:程序需要为
select
函数准备三个文件描述符集合,分别表示要监控的读、写和异常条件。这些集合通常由FD_SET、FD_CLR、FD_ISSET和FD_ZERO这四个宏来操作。 -
调用
select
函数:程序调用select
函数,并传入监控的文件描述符集合。此外,还需要设置一个超时时间,以便在没有任何I/O事件发生时,select
函数能够在超时后返回。 -
等待I/O事件:
select
函数会阻塞,直到至少有一个文件描述符准备好进行I/O操作,或者超时时间到达。 -
检查文件描述符状态:
select
函数返回后,程序需要检查文件描述符集合的状态,以确定哪些文件描述符准备好进行I/O操作。然后,程序可以根据文件描述符的状态来执行相应的读、写或异常处理操作。 -
重复以上过程:在执行完当前的I/O操作后,程序可以再次调用
select
函数,以继续监控文件描述符的状态。
2.select函数详解
为了更好地理解select函数,本节将详细介绍其函数原型、参数解析以及返回值分析。
2.1. 函数原型
在C语言中,select函数的原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
2.2. 参数解析
接下来,我们将逐个解析select函数的参数。
2.2.1. nfds
nfds表示需要监控的文件描述符的最大值加1。这个值通常设为所有文件描述符中的最大值加1,以确保select能够正确地监控所有需要的文件描述符。
2.2.2. readfds, writefds, exceptfds
readfds,writefds和exceptfds分别表示要监控的读、写和异常条件的文件描述符集合。它们是由fd_set类型表示的位图结构。可以使用以下四个宏来操作这些集合:
• FD_SET(fd, &set): 将文件描述符fd添加到set集合中。
• FD_CLR(fd, &set): 从set集合中删除文件描述符fd。
• FD_ISSET(fd, &set): 检查fd是否在set集合中。
• FD_ZERO(&set): 清空set集合。
2.2.3. timeout
timeout参数是一个timeval结构指针,用于设置select函数的超时时间。当timeout为NULL时,select将无限期地等待,直到有文件描述符准备好。当timeout设置为0时,select将立即返回。当timeout设置为非零值时,select将等待指定的时间,直到有文件描述符准备好或超时。
timeval结构如下:
struct timeval {
long tv_sec; // seconds
long tv_usec; // microseconds
};
2.3. 返回值分析
select函数的返回值表示以下三种情况:
1. 返回值大于0:表示有准备好的文件描述符,即已经发生的I/O事件数量。
2. 返回值等于0:表示超时,即在指定的时间内没有任何I/O事件发生。
3. 返回值小于0:表示发生错误。在这种情况下,可以使用perror或strerror函数来获取错误信息。
在调用select函数后,可以通过检查readfds,writefds和exceptfds集合的状态,以确定哪些文件描述符准备好进行I/O操作。然后,程序可以根据文件描述符的状态来执行相应的读、写或异常处理操作。
3.使用IO多路复用完成TCP并发服务器
#include <myhead.h>
#define SERIP "192.168.0.135"
#define SERPORT 8888
#define BACKLOG 10
int main(int argc, const char *argv[])
{
//1创建套接字
int oldfd = socket(AF_INET,SOCK_STREAM,0);
if(oldfd==-1)
{
perror("socket");
return -1;
}
//2启动端口号快速复用
int kkk=9;
if(setsockopt(oldfd,SOL_SOCKET,SO_REUSEADDR,&kkk,sizeof(kkk))==-1)
{
perror("setsockopt");
return -1;
}
printf("端口号快速复用成功\n");
//定义要绑定的服务器Ip和端口号
struct sockaddr_in server = {
.sin_family = AF_INET,
.sin_port = htons(SERPORT),
.sin_addr.s_addr = inet_addr(SERIP)
};
//3绑定IP和端口号
if(bind(oldfd,(struct sockaddr *)&server,sizeof(server))==-1)
{
perror("bind");
return -1;
}
//4监听
if(listen(oldfd,BACKLOG)==-1)
{
perror("listen");
return -1;
}
printf("监听成功\n");
struct sockaddr_in client;
int client_len = sizeof(client);
char buff[1024];
fd_set readfds,temp;//1、定义容器存储描述符
FD_ZERO(&readfds);//2、清空容器
FD_SET(0,&readfds);//3放入输入描述符
FD_SET(oldfd,&readfds);//4、放入台阶在描述符
int maxfd = oldfd;//定义最文件描述符
int newfd;
while(1)
{
temp = readfds;
int res = select(maxfd+1,&temp,NULL,NULL,NULL);//永久阻塞
if(res==0)
{
printf("timeout");
return -1;
}
if(res==-1)
{
perror("select");
return -1;
}
//程序执行至此说明描述符有事件触发了select函数
for(int i = 0;i<=maxfd;i++)//遍历所有的文件描述符
{
if(!FD_ISSET(i,&temp))//描述符不存在集合内继续下次循环
{
continue;
}
if(i==oldfd)//套接字事件解除的阻塞
{
//5接收客户端请求
newfd = accept(oldfd,(struct sockaddr *)&client,&client_len);
if(newfd==-1)
{
perror("accept");
return -1;
}
printf("客户端%s:%d已上线\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
FD_SET(newfd,&readfds);//新客户端产生后将新描述符放入集合
maxfd = newfd>maxfd?newfd:maxfd;//更新最大文件描述符
}
else//newfd产生的解除阻塞
{
bzero(buff,sizeof(buff));//清空数组
int len = recv(newfd,buff,sizeof(buff),0);//接收消息
if(len==0)
{
printf("客户端下线\n");
FD_CLR(i,&readfds);
maxfd = newfd>maxfd?newfd:maxfd;//再次更新最大文件描述符
}
printf("收到信息:%s\n",buff);
strcat(buff,"*_*");
send(newfd,buff,sizeof(buff),0);//回复客户端消息
printf("回复成功\n");
}
}
}
close(oldfd);
return 0;
}