I/O多路复用 - select
程序员文章站
2022-06-13 12:52:51
...
了解select,首先我们先了解一下这两句话
- select只负责等(即并不数据搬迁,不处理数据)
- 等待文件描述符的读就绪或者写就绪
select函数
- select系统调用是用来让我们监视多个程序的文件描述符的变化;
- 程序会停在select这里等,知道被监视的文件描述符至少有一个达到了就绪状态;
函数原型
参数:
- _nfds : 表示需要监视的最大文件描述符 + 1;
- __restrict __readfds : 需要检测的可读文件描述符的集合,(输入输出型参数; 输入:用户想关心那些文件描述符上的读事件, 输出: 所关心的文件描述符那些已经读就绪了);
- __restrict __writefds : 需要检测的可写文件描述符的集合,(输入输出型参数; 输入:用户想关心那些文件描述符上的写事件,输出: 所关心的文件描述符那些已经写就绪了);
- __restrict __exceptfds : 需要检测的异常文件描述符的集合,,(输入输出型参数; 输入:用户想关心那些文件描述符上的异常事件,输出: 所关心的文件描述符那些已经异常就绪了);
- __restrict __timeout : 用来设置select等待时间的
timeout的取值:
- NULL : 表示当前select()没有timeout,select将会阻塞的等待文件描述符就绪;
- 0 : 仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生; (非阻塞)
- 一个特定的时间值 : 如果在指定时间内还没有等待到就绪的文件描述符,select()就会超时返回;
fd_set 结构体:
其实这个结构体就是一个位图,使用位图中的对应位表示要监视的文件描述符。
fd_set的结构:
void FD_CLR(int fd, fd_set* set); // 将位图中的某一位置为0
void FD_SET(int fd, fd_set* set); // 将位图中的某一位置为1
int FD_ISSET(int fd, fd_set* set; // 检测位图中fd的位置是不是1
void FD_ZERO(fd_set* set); // 将set中全部位置为0
禁止自己使用与或非方式操作位图!!!
timeval结构体:
这个结构体构建了一个时间, tv_sec是秒数, tv_usec是微妙, 最后时间为 tv_sec+tv_usec;
函数返回值:
- 执行成功返回已改变状态的文件描述符个数;
- 0 : 表示在文件描述符状态改变之前就已经超时了,没有返回;
- -1 : 表示有错误发生,错误原因存在于errno,此时函数的额后四个参数的值变为不可预测;
socket就绪条件
读就绪
- socket内核中, 接收缓冲区中的字节数, ⼤于等于低⽔位标记SO_RCVLOWAT. 此时可以⽆阻塞的读该⽂件描述符, 并且返回值⼤于0;
- socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求;
- socket上有未处理的错误;
写就绪
- socket内核中, 发送缓冲区中的可⽤字节数(发送缓冲区的空闲位置⼤⼩), ⼤于等于低⽔位标记 SO_SNDLOWAT, 此时可以⽆阻塞的写, 并且返回值⼤于0;
- socket的写操作被关闭(close或者shutdown). 对⼀个写操作被关闭的socket进⾏写操作, 会触发 SIGPIPE信号;
- socket使⽤⾮阻塞connect连接成功或失败之后;
- socket上有未读取的错误;
异常就绪
- socket收到带外数据(可以联想到TCP中的紧急指针URG);
select特点
- 可监控的文件描述符个数取决于sizeof(fd_set)的值,我这边是128,每个比特位上一个文件描述符,我的服务器上最大就可表示 128*8 = 1024个;
- 将fd加入到select监控集的同时,还需要再使用一个数据结构arr来保存select监控集中的fd
一是用于select返回后,arr作为源数据和fd进行FD_ISSET判断;
二是select返回后,会把以前加入的但是没有事件发生的fd清空,则每次开始select前都要重新从arr取得一个fd逐一加入,扫描arr的同时取得fd的maxfd,用于select的第一个参数;
代码编写: select服务器(回显)
// select_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>
// 对fd_set进行一个简单的封装
// 为了能够方便的获取到文件描述符集中的的最大文件描述符
typedef struct FdSet
{
fd_set fds;
int max_fd; // 当前文件描述符集中的最大文件描述符
}FdSet;
void InitFdSet(FdSet* set)
{
if(set == NULL)
{
return;
}
set->max_fd = -1;
FD_ZERO(&set->fds);
}
void AddFdSet(FdSet* set,int fd)
{
FD_SET(fd,&set->fds);
if(fd > set->max_fd)
{
set->max_fd = fd;
}
}
void DelFdSet(FdSet* set,int fd)
{
FD_CLR(fd,&set->fds);
int max_fd = -1;
int i = 0;
for(; i <= set->max_fd; ++i)
{
if(!FD_ISSET(i,&set->fds))
{
continue;
}
if(i > max_fd)
{
max_fd = i;
}
}
// 循环结束以后,max_fd就是当前最大的文件描述符
set->max_fd = max_fd;
return;
}
//////////////////////////////////////////////////// 封装完成
int ServerStart(short port)
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(port);
if(bind(fd,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
return -2;
}
if(listen(fd,3) < 0)
{
perror("listen");
return -3;
}
return fd;
}
int ProcessRequest(int new_sock)
{
// 完成对客户端的读写操作
// 这里不可以向以前一样进行循环读写操作
// 当前是单进程/线程程序,如果死循环就无法执行别的操作了
// 此处只进行一次读写,因为我们要将所有的等待操作都交给select完成
// 下一次对该 socket 读取的时机,也是由select来进行通知的
// 也就是select下一次返回该文件描述符就绪的时候就能进行读数据了
char buf[1024] = {0};
ssize_t read_size = read(new_sock,buf,sizeof(buf)-1);
if(read_size < 0)
{
perror("read");
return -1;
}
else if(read_size == 0)
{
printf("[client %d] : disconnect\n",new_sock);
return 0;
}
buf[read_size] = '\0';
printf("[client %d] : %s\n",new_sock,buf);
write(new_sock,buf,strlen(buf));
return read_size;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
printf("Usage: %s [port]\n",argv[0]);
return 1;
}
// 1.对服务器进行初始化
int listen_sock = ServerStart(atoi(argv[1]));
if(listen_sock < 0)
{
printf("ServerStart faild\n");
return 1;
}
printf("ServerStart OK\n");
// 2.进入事件循环
FdSet read_fds; // 输入参数: 要等待的所有文件描述符都要加到这个位图中
InitFdSet(&read_fds);
AddFdSet(&read_fds,listen_sock);
while(1)
{
// 3.使用select完成等待
// 创建 output_fds 的目的是: 防止select返回,把read_fds的内容给破坏掉,导致期中数据丢失
// read_fds 始终表示select监控的文件描述符集的内容
FdSet output_fds = read_fds; // 输出型参数: 用来保存每一次的输出参数
int ret = select(output_fds.max_fd+1,&output_fds.fds,NULL,NULL,NULL);
if(ret < 0)
{
perror("select");
continue;
}
// 4.select返回以后进行处理
// a) listen_sock 读就绪
// b) new_sock 读就绪
if(FD_ISSET(listen_sock,&output_fds.fds))
{// 查看listen_sock在位图中,就证明客户端连接上了,可以accept了
// 调用 accept 获取到连接,将new_sock加入到select之中
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
if(new_sock < 0)
{
perror("accept");
return 1;
}
AddFdSet(&read_fds,new_sock);
printf("[client : %d] connect \n",new_sock);
}
else
{// 当前就是 new_sock 就绪了
int i = 0;
for(; i < output_fds.max_fd + 1; ++i)
{
if(!FD_ISSET(i,&output_fds.fds))
{
continue;
}
// 到了这里,证明 i 是位图中的一位, 就可以开始读写了
int ret = ProcessRequest(i);
if(ret <= 0)
{
// 到这里正明客户端已经断开连接了
// close DelFdSet 两步顺序不影响
DelFdSet(&read_fds,i);
close(i);
} // end if(ret<=0)
} // end for
} // end else
} // end while(1)
return 0;
}
客户端的代码和之前实现tcp/udp一样
// select_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("Usage: ./client [ip] [port]\n");
return 1;
}
// 1. 创建 socket
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd < 0)
{
perror("socket");
return 1;
}
// 2. 建立连接
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(atoi(argv[2]));
int ret = connect(fd,(sockaddr*)&server,sizeof(server));
if(ret < 0)
{
perror("connect");
return 1;
}
// 3. 进入事件循环
while(1)
{
// a) 从标准输入读数据
char buf[1024] = {0};
ssize_t read_size = read(0,buf,sizeof(buf)-1);
if(read_size < 0)
{
perror("read");
return 1;
}
if(read_size == 0)
{
printf("read done\n");
return 0;
}
buf[read_size] = '\0';
// b) 把读入的数据发送到服务器上
write(fd,buf,strlen(buf));
// c) 从服务器读取相应结果
char buf_resp[1024] = {0};
read_size = read(fd,buf_resp,sizeof(buf_resp) - 1);
if(read_size < 0)
{
perror("read");
return 1;
}
else if(read_size == 0)
{
//对端先断开连接
printf("server close socket\n");
return 0;
}
buf_resp[read_size] = '\0';
// d) 把结果打印到标准输出上
printf("server respond > %s\n",buf_resp);
}
return 0;
}
观察以上代码
如果当前的文件描述符是 3,5,7,9
当前 3 是 listen_sock
返回结果为 3,5
但是只处理了3,这时候 5 会怎么样呢? 会不会处理不到?
水平触发:
如果一个文件描述符就绪了,但是这一次没有处理到这个文件描述符
但是下一次select仍但会获取到这个文件描述符,把它处理掉
select缺点
- 每次调用select都需要重新手动设置fd集合;
- 接口使用非常不方便;
- 每次调用select,都需要把fd集合从用户态拷贝至内核,当fd很多的时候开销会非常大;
- 每次调用fd都需要在内核中遍历传递进来的所有fd,当fd很多的时候开销也会非常大;
- select支持的文件描述符太少了;