IO多路转接 —— select
创始人
2024-05-24 10:32:33
0

文章目录

    • 1. select的概念
    • 2. select的函数接口
    • 3. 基于select的简易服务器实现
    • 4. select优缺点分析

前言: select用的少现在,而且写起来很复杂,本篇所讲的select IO模型主要是用于处理读就绪事件的,至于写就绪不关心。


1. select的概念

select函数来实现多路复用输入/输出模型:

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

select函数它是负责等这个过程的,等成功了就通知上层来缓冲区拷贝,这个等的方式是可以自己设置的,比如:阻塞等,非阻塞等都行。注意:它只负责等,不负责拷贝或者写入。

2. select的函数接口

在这里插入图片描述

  • int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

函数参数:

  1. nfds 是要监测的fd中,最大的fd+1,它相当于一个边界控制。
  2. fd_set *readfds, fd_set *writefds, fd_set *exceptfds,这三个参数是输入输出型参数,输入:你告诉内核你要关心那些fd。输出:你关心的fd有谁就绪了。总共有三个参数,它们分别是可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。
  3. timeout这个结构体就是一个时间线。就用它来控制等的方式,如果将它给成null,那么就是阻塞式等。如果给成{0,0},那就是非阻塞式等。如果给成{5,0},那就是5秒前为阻塞式等,五秒后为非阻塞式等(相当于超时处理)。

函数返回值:

  • 大于 0:成功,返回集合中已就绪的文件描述符的总个数
  • 等于 - 1:函数调用失败
  • 等于 0:超时,没有检测到就绪的文件描述符

fd_set: 这个类型它就是一个位图,可以通过操作位图来表示你要关心哪些fd。它的大小是128字节。128*8=1024个比特位,也就是说可以检测1024个文件描述符,这个范围其实不是很大。

看一下它的结构:

typedef struct{/* XPG4.2 requires this member name.  Otherwise avoid the namefrom the global namespace.  */
#ifdef __USE_XOPEN__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif} fd_set;

但这个位图,不能直接操作,必须使用函数接口:

  • void FD_CLR(int fd, fd_set *set); // 这是用于去除位图set中的某个fd
  • int FD_ISSET(int fd, fd_set *set);// 用来检测某个fd是否在位图set中
  • void FD_SET(int fd, fd_set *set); // 把某个fd加入到位图set中
  • void FD_ZERO(fd_set *set);//清空位图

举个例子,就取fd_set的最后8个比特位:fd_set i;

(1)先是利用FD_ZERO(&i)清空位图。
在这里插入图片描述
(2)将fd = 5,设置到位图i中,FD_SET(5,&i)。那就是将第五个比特位设置为1。
在这里插入图片描述
(3)再把fd = 1,fd =2 ,都设置进去:

在这里插入图片描述
(4)那么就可以利用select进行检测,假如这些描述符组合,我只关心读事件就绪:
select(6,&i,null,null,null),为啥是6,最大文件描述符fd是5,5+1 = 6。最后一个参数设置为null,就是阻塞式等待。

(5)假如现在fd=5的读事件就绪了,但是2和1都不就绪,那也返回了,位图就变为:

在这里插入图片描述
注意了:2和1处都被抹除,只有5处为1,因为只有5的读事件就绪了。那么肯定有疑问了,我本来要关心fd=1,2,5,三个文件描述符,虽然只有5事件读就绪,你把1,2都给抹除了,那么下次我还要重新设置位图吗?还得把1和2重新设置进位图?答案是:需要,这就是select难的地方。得有一个数组,提前把你要关心的文件描述符存进去。

那么我是如何知道5事件就绪了呢?还得操作位图:FD_ISSET(5, &i),这就是帮助检测的,如果返回值是>0,那么就说明关系的fd事件已经就绪,<0,就表示未就绪。

(6) 要是不关心某个fd了,就可以用void FD_CLR(int fd, fd_set *set);

3. 基于select的简易服务器实现

那么知道了select的基本操作函数,我们就基于select来完成一个简易的读取数据服务器,客户端就不写了,我们主要看的是服务器中使用select的基本逻辑。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include  这是在定义一个存放fd集合的数组,记忆功能
#define NUM (sizeof(fd_set) * 8)
int fd_array[NUM];void useage()
{std::cout << "please use"<< "./select_sever"<< "+"<< "端口号" << std::endl;
}int main(int argv, char *argc[])
{ 启动服务器的方式if (argv != 2){useage();exit(1);} 使服务器进去listen状态int listen_fd = socket(AF_INET, SOCK_STREAM, 0);if (listen_fd < 0){std::cerr << "listen_fd failed" << std::endl;exit(2);}std::cout << "listen_fd:" << listen_fd << std::endl;struct sockaddr_in my_sock;my_sock.sin_family = AF_INET;my_sock.sin_port = htons(atoi(argc[1]));my_sock.sin_addr.s_addr = INADDR_ANY;if (bind(listen_fd, (const struct sockaddr *)&my_sock, sizeof(my_sock)) < 0){std::cerr << "bind errno" << std::endl;exit(3);}if (listen(listen_fd, 5) < 0){std::cerr << "listen errno" << std::endl;exit(4);} 请问这里可以accept吗?绝对不可以,因为accept它是从listen_fd中拿连接,这也是读取事件,所以要用select进行管理。 初始化一下 fd_arryfor (int i = 0; i < NUM; i++){fd_array[i] = -1;} 我们只关心读事件,那么先得有一个位图fd_Setfd_set rfds;fd_array[0] = listen_fd; // 这个是不变的,一上来就只有一个套接字要监听读事件,那就是listen_fdwhile (true){ 清空位图FD_ZERO(&rfds); 把关心的事件设置进位图,并且更新MAX_fd(select的第一个参数)int MAX_fd = fd_array[0];for (int i = 0; i < NUM; i++){if (fd_array[i] == -1)continue;FD_SET(fd_array[i], &rfds);if (MAX_fd < fd_array[i]){MAX_fd = fd_array[i];}}开始select等待int n = select(MAX_fd + 1, &rfds, NULL, NULL, NULL);switch (n){case -1:std::cerr << "select error" << std::endl;break;case 0:std::cout << "time out" << std::endl;break;default:std::cout << "有读事件就绪" << std::endl;std::cout<<"********************************************"<if (fd_array[i] == -1)continue;if (FD_ISSET(fd_array[i], &rfds)){if (fd_array[i] == listen_fd){std::cout << "有新的连接" << std::endl;// 进行连接struct sockaddr_in user;socklen_t size_sockaddr = sizeof(user);int sock = accept(listen_fd, (struct sockaddr *)&user, &size_sockaddr);if (sock < 0){std::cerr << "accept errno" << std::endl;exit(5);}std::cout<<"连接成功:"<<"fd = "<if (fd_array[pos] == -1)break;}// 对pos位置进行管理,看pos位置是否合法:if (pos < NUM){fd_array[pos] = sock;}else{std::cout << "服务器满载,关闭此连接" << std::endl;std::cout<<"********************************************"< 走到这里,说明就是普通的读取事件,普通套接字的读事件就绪。 到这可以读吗?当然可以,人家都通知你读了std::cout << "套接字为" << fd_array[i] << "有读取事件就绪" << std::endl;char buffer[1024] = {0};ssize_t N = recv(fd_array[i], buffer, sizeof(buffer) - 1, 0);if (N > 0){buffer[N] = 0;std::cout << "client[" << fd_array[i] << "]#" << buffer << std::endl;std::cout<<"********************************************"<std::cout << "对端连接关闭" << std::endl;close(fd_array[i]);// 注意,我们要把它在数组中的存储一并去除,这件事很重要fd_array[i] = -1;std::cout<<"********************************************"<std::cout << "读取失败"<< "主动关闭连接" << std::endl;close(fd_array[i]);// 注意,我们要把它在数组中的存储一并去除,这件事很重要fd_array[i] = -1;std::cout<<"********************************************"<

看看结果:
(1) 服务器起来
在这里插入图片描述
(2) telnet 连接

在这里插入图片描述
(3) 看现象,这是连接成功了,并且被select管理起来的
在这里插入图片描述
(4) 客户端发送数据
在这里插入图片描述
(5) 服务器接收
在这里插入图片描述
(6) 客户端退出,服务端:
在这里插入图片描述

至于这个代码的讲解,我都在注释里讲清楚了,不好理解。

其实,里面的select默认用的是阻塞等待,如果想要实验 select 的等待方式,其实只需要把函数的最后参数 timeout 改改就可以了。

4. select优缺点分析

分析一下select IO模型,它非常依赖第三方数组,原因有两点:

  1. fd_set 位图每一次都需要重新设置,这是很尴尬的,我让你关心这些fd事件,你返回的时候只返回哪些就绪了的,其余都给我抹除了,我还得搞个第三方数组,提前把关心的事件保存起来。
  2. 你要想关心别的fd,或者不再关心某个fd,都需要第三方数组进行管理。

那么就可以总结一下它的优缺点:

  • 优点:一次可以等待多个fd,一定程度上提高了IO效率
  • 缺点:每次都要重新设置位图,还得遍历检测。select从用户层到内核,多次切换,这个频率有点高,这是有开销的。文件描述符1024个,这个数量有点小。

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...