#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select() 是一个系统调用函数,用于在多个文件描述符上进行 I/O 多路复用。通过 select() 函数,可以监视多个文件描述符的状态,以确定是否有读写事件准备就绪。
入参:
select() 函数会阻塞当前进程,直到满足以下条件之一:
返回值:
fd_set 是一个数据结构,用于表示文件描述符的集合。它是一个位图,每个文件描述符在 fd_set 中占据一个位,用于标识该文件描述符的状态。
typedef struct fd_set {
unsigned int fd_count; // 文件描述符的数量
int fd_array[FD_SETSIZE]; // 文件描述符数组
} fd_set;
其中,fd_count 表示文件描述符的数量,fd_array 是一个数组,用于存储文件描述符的值。
fd_set 数据结构是一个固定大小的数组,其大小由宏 FD_SETSIZE 定义。在大多数系统中,FD_SETSIZE 的默认值是 1024,因此 fd_set 可以容纳的文件描述符数量通常是有限的。如果需要监听更多的文件描述符,可能需要对 FD_SETSIZE 进行修改或使用其他更高效的多路复用机制。
fd_set 提供了一些宏函数来操作文件描述符集合,常用的宏函数有:
FD_ZERO(fd_set *set):将文件描述符集合中的所有位清零。
FD_SET(int fd, fd_set *set):将指定的文件描述符加入文件描述符集合。
FD_CLR(int fd, fd_set *set):从文件描述符集合中移除指定的文件描述符。
FD_ISSET(int fd, fd_set *set):检查指定的文件描述符是否在文件描述符集合中。
timeval 是一个结构体,用于表示时间值(time value)。
struct timeval {
long tv_sec; // 秒数
long tv_usec; // 微秒数
};
这个参数有以下三种可能:
服务端使用select函数的基本流程:
1、创建并初始化套接字:服务端需要创建一个监听套接字,用于接受客户端的连接请求。同时,需要将监听套接字添加到select监视的文件描述符集中。
2、设置文件描述符集:在使用select之前,需要将所有需要监视的文件描述符(包括监听套接字和已连接的客户端套接字)添加到文件描述符集中。可以使用FD_SET宏将套接字添加到集合中。
3、调用select函数:使用select函数开始监听文件描述符集中的事件。select函数会阻塞程序执行,直到集合中的文件描述符有可读、可写或异常事件发生。
4、处理就绪的文件描述符:当select函数返回后,需要遍历文件描述符集,确定哪些套接字上发生了事件。可以使用FD_ISSET宏来检查文件描述符是否准备就绪。
5、处理连接请求:如果监听套接字(通常是服务器套接字)准备就绪,表示有新的客户端连接请求。此时,可以调用accept函数接受客户端的连接,并将其加入到文件描述符集中进行监视。
6、处理客户端数据:如果已连接的客户端套接字准备就绪,表示有数据可读或可写。此时,可以使用recv函数读取客户端发送的数据,或使用send函数向客户端发送数据。
7、循环监听:在处理完所有就绪的文件描述符后,可以再次调用select函数,继续监听新的事件。可以使用循环来反复执行这个过程,以实现持续的事件驱动。
#include
//socket
#include
#include
//close
#include
//exit
#include
//perror
#include
//memset
#include
//htons
#include
//select
#include
/* According to earlier standards */
#include
#define PORT 8596
#define MESSAGE_SIZE 1024
#define FD_SIZE 1024
int main(){
int ret=-1;
int socket_fd=-1;
int accept_fd=-1;
int accept_fds[FD_SIZE]={-1,};
//可用fd索引
int canUseFDIndex=-1;
//最大的fd索引
int maxFDIndex=0;
int max_fd=-1;
fd_set fd_sets;
int ready=0;
int backlog=10;
int flags=1;
struct sockaddr_in local_addr,remote_addr;
//create socket
socket_fd=socket(AF_INET,SOCK_STREAM,0);
if(socket_fd == -1){
perror("create socket error");
exit(1);
}
//set option of socket
ret = setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags));
if ( ret == -1 ){
perror("setsockopt error");
}
//set socket address
local_addr.sin_family=AF_INET;
local_addr.sin_port=htons(PORT);
local_addr.sin_addr.s_addr=INADDR_ANY;
bzero(&(local_addr.sin_zero),8);
//bind socket
ret=bind(socket_fd, (struct sockaddr *)&local_addr,sizeof(struct sockaddr_in));
if(ret == -1){
perror("bind socket error");
exit(1);
}
ret=listen(socket_fd, backlog);
if(ret ==-1){
perror("listen error");
exit(1);
}
//重置max_fd;
max_fd=socket_fd;
for(int i=0;i < FD_SIZE;i++){
accept_fds[i]=-1;
}
//loop to accept client
for(;;){
//清空
FD_ZERO(&fd_sets);
//socket_fd加入集合
FD_SET(socket_fd,&fd_sets);
//同步集合中最大的文件描述符
for(int j=0;j<maxFDIndex;j++){
if(accept_fds[j] !=-1){
if(accept_fds[j] > max_fd){
max_fd=accept_fds[j];
}
//重新加入需要监听的文件描述符到集合里
FD_SET(accept_fds[j],&fd_sets);
}
}
struct timeval timeout;
timeout.tv_sec = 5; // 设置超时时间为 5 秒
timeout.tv_usec = 0;
ready=select(max_fd+1,&fd_sets,nullptr,nullptr,timeout);
if(ready<0){
perror("error in select");
break;
}else if(ready==0){
perror("select time out!");
continue;
}else if(ready){
printf("ready:%d\n",ready);
//socket有新的连接请求
if(FD_ISSET(socket_fd,&fd_sets)){
//找到没有使用的位置
int k=0;
for(;k<FD_SIZE;k++){
if(accept_fds[k] == -1){
canUseFDIndex=k;
break;
}
}
if(k==FD_SIZE){
perror("the connected is full!\n");
continue;
}
socklen_t addrlen = sizeof(remote_addr);
accept_fd=accept(socket_fd,( struct sockaddr *)&remote_addr, &addrlen);
accept_fds[canUseFDIndex]=accept_fd;
if(canUseFDIndex+1 >maxFDIndex){
maxFDIndex=canUseFDIndex+1;
}
//同步最大文件描述符
if(accept_fd > max_fd){
max_fd=accept_fd;
}
}
for(int p=0;p<maxFDIndex;p++){
if(accept_fds[p] !=-1 && FD_ISSET(accept_fds[p],&fd_sets)){
char in_buf[MESSAGE_SIZE]={0,};
memset(in_buf,0,MESSAGE_SIZE);
//read data
int ret =recv(accept_fds[p], (void*)in_buf, MESSAGE_SIZE, 0);
if(ret ==0){
close(accept_fds[p]);
accept_fds[p]=-1;
break;
}
printf("receive data:%s\n",in_buf);
send(accept_fds[p], (void *)in_buf, MESSAGE_SIZE, 0);
}
}
}
}
printf("quit server....");
close(socket_fd);
return 0;
}
select是一种用于多路复用(multiplexing)的系统调用,常用于实现异步I/O操作。它在编程中具有一些优点和缺点:
优点:
高效的事件驱动:select允许程序同时监视多个文件描述符(如套接字),并在其中任何一个文件描述符准备好进行I/O操作时通知程序。这种事件驱动的方式可以提高程序的效率,避免了不必要的忙等待。
跨平台兼容性:select是标准的POSIX接口,因此在大多数主流操作系统上都有良好的支持。这使得可以使用相同的代码在不同的平台上进行开发,提高了可移植性。
简单易用:select的接口相对简单,适用于处理少量的文件描述符。它使用简洁的参数和返回值,易于理解和使用。对于简单的I/O多路复用需求,select是一个较为直观的选择。
缺点:
低效的扩展性:select的一个主要缺点是其在处理大量文件描述符时的低效性。它采用线性扫描的方式遍历所有待监视的文件描述符,当文件描述符数量较大时,性能会明显下降。
需要维护文件描述符集:使用select需要维护一个文件描述符集,包含所有要监视的文件描述符。这要求开发人员在程序中维护一个数据结构来管理这些文件描述符,增加了一定的复杂性。
不支持高级特性:相比其他更高级的I/O多路复用机制(如epoll、kqueue等),select的功能相对有限。它不支持一些高级特性,如边缘触发(edge-triggered)模式和自动扩展等。
总体而言,select是一种简单易用且可移植的多路复用机制,适用于处理少量文件描述符的情况。然而,在高并发和大规模的I/O操作中,可能需要考虑其他更高效和功能更强大的替代方案。