一、select头文件
cpp
#include <sys/select.h>
二、select原型
cpp
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select() 是一个系统调用函数 ,用于在多个文件描述符上进行 I/O 多路复用 。通过 select() 函数,可以监视多个文件描述符的状态 ,以确定是否有读写事件准备就绪。
入参:
- nfds:要检查的最大文件描述符值加 1。
- readfds:用于检查可读性的文件描述符集合。
- writefds:用于检查可写性的文件描述符集合。
- exceptfds:用于检查异常条件的文件描述符集合。
- timeout:超时时间,指定 select() 调用的最长等待时间。
select() 函数会阻塞当前进程,直到满足以下条件之一:
- 有一个或多个文件描述符准备好进行读操作。
- 有一个或多个文件描述符准备好进行写操作。
- 发生了异常情况,如带外数据到达。
返回值:
- 如果返回值大于 0:表示有文件描述符就绪,且返回值是就绪文件描述符的总数。
- 如果返回值等于 0:表示超时,即在指定的超时时间内没有文件描述符就绪。
- 如果返回值等于 -1:表示出现错误,可以通过查看 errno 变量来获取具体的错误信息。
2.1、fd_set
fd_set 是一个数据结构,用于表示文件描述符的集合。它是一个位图,每个文件描述符在 fd_set 中占据一个位,用于标识该文件描述符的状态。
cpp
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) :检查指定的文件描述符是否在文件描述符集合中。
2.2、timeval
timeval 是一个结构体,用于表示时间值(time value)。
cpp
struct timeval {
long tv_sec; // 秒数
long tv_usec; // 微秒数
};
这个参数有以下三种可能:
- 永远等待
仅在一个描述符准备好I/O时才返回,为此,我们可以把该参数置为空指针 - 等待一段固定时间
在有一个描述符准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数 - 根本不等待
检查描述符后立即返回,这称为"轮询"。该参数必须指向一个timeval结构,并且其中的定时器值(秒数和微秒数)必须为0.
三、代码实现
服务端使用select函数的基本流程:
1、创建并初始化套接字:服务端需要创建一个监听套接字,用于接受客户端的连接请求。同时,需要将监听套接字添加到select监视的文件描述符集中。
2、设置文件描述符集:在使用select之前,需要将所有需要监视的文件描述符(包括监听套接字和已连接的客户端套接字)添加到文件描述符集中。可以使用FD_SET宏将套接字添加到集合中。
3、调用select函数:使用select函数开始监听文件描述符集中的事件。select函数会阻塞程序执行,直到集合中的文件描述符有可读、可写或异常事件发生。
4、处理就绪的文件描述符:当select函数返回后,需要遍历文件描述符集,确定哪些套接字上发生了事件。可以使用FD_ISSET宏来检查文件描述符是否准备就绪。
5、处理连接请求:如果监听套接字(通常是服务器套接字)准备就绪,表示有新的客户端连接请求。此时,可以调用accept函数接受客户端的连接,并将其加入到文件描述符集中进行监视。
6、处理客户端数据:如果已连接的客户端套接字准备就绪,表示有数据可读或可写。此时,可以使用recv函数读取客户端发送的数据,或使用send函数向客户端发送数据。
7、循环监听:在处理完所有就绪的文件描述符后,可以再次调用select函数,继续监听新的事件。可以使用循环来反复执行这个过程,以实现持续的事件驱动。
cpp
#include <iostream>
//socket
#include <sys/types.h>
#include <sys/socket.h>
//close
#include <unistd.h>
//exit
#include <stdlib.h>
//perror
#include <stdio.h>
//memset
#include <string.h>
//htons
#include <arpa/inet.h>
//select
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#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操作中,可能需要考虑其他更高效和功能更强大的替代方案。