IO多路转接之select

IO多路转接之select

  • 1. IO多路转接(复用)
  • 2. select
    • 2.1 函数原型
    • 2.2 细节描述
  • 3. 并发处理
    • 3.1 处理流程
    • 3.2 通信代码

原文链接

1. IO多路转接(复用)

IO多路转接也称为IO多路复用,它是一种网络通信的手段(机制),通过这种方式可以同时监测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪( 可以读数据或者可以写数据)程序的阻塞就会被解除,之后就可以基于这些(一个或多个)就绪的文件描述符进行通信了。通过这种方式在单线程/进程的场景下也可以在服务器端实现并发。常见的IO多路转接方式有:selectpollepoll

下面先对多线程/多进程并发和IO多路转接的并发处理流程进行对比(服务器端):

  • 多线程/多进程并发
    • 主线程/父进程:调用 accept()监测客户端连接请求
      • 如果没有新的客户端的连接请求,当前线程/进程会阻塞
      • 如果有新的客户端连接请求解除阻塞,建立连接
  • 子线程/子进程:和建立连接的客户端通信
    • 调用 read() / recv() 接收客户端发送的通信数据,如果没有通信数据,当前线程/进程会阻塞,数据到达之后阻塞自动解除
    • 调用 write() / send() 给客户端发送数据,如果写缓冲区已满,当前线程/进程会阻塞,否则将待发送数据写入写缓冲区中
  • IO多路转接并发
    • 使用IO多路转接函数委托内核检测服务器端所有的文件描述符(通信和监听两类),这个检测过程会导致进程/线程的阻塞,如果检测到已就绪的文件描述符阻塞解除,并将这些已就绪的文件描述符传出
    • 根据类型对传出的所有已就绪文件描述符进行判断,并做出不同的处理
      • 监听的文件描述符:和客户端建立连接
        • 此时调用accept()是不会导致程序阻塞的,因为监听的文件描述符是已就绪的(有新请求)
      • 通信的文件描述符:调用通信函数和已建立连接的客户端通信
        • 调用 read() / recv() 不会阻塞程序,因为通信的文件描述符是就绪的,读缓冲区内已有数据
        • 调用 write() / send() 不会阻塞程序,因为通信的文件描述符是就绪的,写缓冲区不满,可以往里面写数据
    • 对这些文件描述符继续进行下一轮的检测(循环往复。。。)

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

2. select

2.1 函数原型

使用select这种IO多路转接方式需要调用一个同名函数select,这个函数是跨平台的,Linux、Mac、Windows都是支持的。程序猿通过调用这个函数可以委托内核帮助我们检测若干个文件描述符的状态,其实就是检测这些文件描述符对应的读写缓冲区的状态:

  • 读缓冲区:检测里边有没有数据,如果有数据该缓冲区对应的文件描述符就绪
  • 写缓冲区:检测写缓冲区是否可以写(有没有容量),如果有容量可以写,缓冲区对应的文件描述符就绪
  • 读写异常:检测读写缓冲区是否有异常,如果有该缓冲区对应的文件描述符就绪

委托检测的文件描述符被遍历检测完毕之后,已就绪的这些满足条件的文件描述符会通过select()的参数分3个集合传出,程序猿得到这几个集合之后就可以分情况依次处理了。

下面来看一下这个函数的函数原型:

#include <sys/select.h>
struct timeval {time_t      tv_sec;         /* seconds */suseconds_t tv_usec;        /* microseconds */
};int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval * timeout);
  • 函数参数:
    • nfds:委托内核检测的这三个集合中最大的文件描述符 + 1
      • 内核需要线性遍历这些集合中的文件描述符,这个值是循环结束的条件
      • 在Window中这个参数是无效的,指定为-1即可
    • readfds:文件描述符的集合, 内核只检测这个集合中文件描述符对应的读缓冲区
      • 传入传出参数,读集合一般情况下都是需要检测的,这样才知道通过哪个文件描述符接收数据
    • writefds:文件描述符的集合, 内核只检测这个集合中文件描述符对应的写缓冲区
      • 传入传出参数,如果不需要使用这个参数可以指定为NULL
    • exceptfds:文件描述符的集合, 内核检测集合中文件描述符是否有异常状态
      • 传入传出参数,如果不需要使用这个参数可以指定为NULL
    • timeout:超时时长,用来强制解除select()函数的阻塞的
      • NULL:函数检测不到就绪的文件描述符会一直阻塞。
      • 等待固定时长(秒):函数检测不到就绪的文件描述符,在指定时长之后强制解除阻塞,函数返回0
      • 不等待:函数不会阻塞,直接将该参数对应的结构体初始化为0即可。
  • 函数返回值:
    • 大于0:成功,返回集合中已就绪的文件描述符的总个数
    • 等于-1:函数调用失败
    • 等于0:超时,没有检测到就绪的文件描述符

另外初始化fd_set类型的参数还需要使用相关的一些列操作函数,具体如下:

// 将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0        
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int  FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);

2.2 细节描述

在select()函数中第2、3、4个参数都是fd_set类型,它表示一个文件描述符的集合,类似于信号集 sigset_t,这个类型的数据有128个字节,也就是1024个标志位,和内核中文件描述符表中的文件描述符个数是一样的。

sizeof(fd_set) = 128 字节 * 8 = 1024 bit      // int [32]

这并不是巧合,而是故意为之。这块内存中的每一个bit 和 文件描述符表中的每一个文件描述符是一一对应的关系,这样就可以使用最小的存储空间将要表达的意思描述出来了。

下图中的fd_set中存储了要委托内核检测读缓冲区的文件描述符集合。

  • 如果集合中的标志位为0代表不检测这个文件描述符状态
  • 如果集合中的标志位为1代表检测这个文件描述符状态

在这里插入图片描述

内核在遍历这个读集合的过程中,如果被检测的文件描述符对应的读缓冲区中没有数据,内核将修改这个文件描述符在读集合fd_set中对应的标志位,改为0,如果有数据那么这个标志位的值不变,还是1
在这里插入图片描述

select()函数解除阻塞之后,被内核修改过的读集合通过参数传出,此时集合中只要标志位的值为1,那么它对应的文件描述符肯定是就绪的,我们就可以基于这个文件描述符和客户端建立新连接或者通信了。

3. 并发处理

3.1 处理流程

如果在服务器基于select实现并发,其处理流程如下:

  1. 创建监听的套接字 lfd = socket();
  2. 将监听的套接字和本地的IP和端口绑定 bind()
  3. 给监听的套接字设置监听 listen()
  4. 创建一个文件描述符集合 fd_set,用于存储需要检测读事件的所有的文件描述符
    • 通过 FD_ZERO() 初始化
    • 通过 FD_SET() 将监听的文件描述符放入检测的读集合中
  5. 循环调用select(),周期性的对所有的文件描述符进行检测
  6. select() 解除阻塞返回,得到内核传出的满足条件的就绪的文件描述符集合
    • 通过FD_ISSET() 判断集合中的标志位是否为 1
      • 如果这个文件描述符是监听的文件描述符,调用 accept() 和客户端建立连接
        • 将得到的新的通信的文件描述符,通过FD_SET() 放入到检测集合中
      • 如果这个文件描述符是通信的文件描述符,调用通信函数和客户端通信
        • 如果客户端和服务器断开了连接,使用FD_CLR()将这个文件描述符从检测集合中删除
        • 如果没有断开连接,正常通信即可
  7. 重复第6步
    在这里插入图片描述

3.2 通信代码

select_server.cpp

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>int main()
{// 1. 创建监听的fdint lfd = socket(AF_INET, SOCK_STREAM, 0);// 2. 绑定struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(9999);addr.sin_addr.s_addr = INADDR_ANY;bind(lfd, (struct sockaddr*)&addr, sizeof(addr));// 3. 设置监听listen(lfd, 128);// 将监听的fd的状态检测委托给内核检测int maxfd = lfd;// 初始化检测的读集合fd_set rdset;fd_set rdtemp;// 清零FD_ZERO(&rdset);// 将监听的lfd设置到检测的读集合中FD_SET(lfd, &rdset);// 通过select委托内核检测读集合中的文件描述符状态, 检测read缓冲区有没有数据// 如果有数据, select解除阻塞返回// 应该让内核持续检测while(1){// 默认阻塞// rdset 中是委托内核检测的所有的文件描述符rdtemp = rdset;// 这里timeout参数一定要设置为结构体,要么就设为NULL,NULL就是默认阻塞struct timeval timeout;timeout.tv_sec = 0;timeout.tv_usec = 0;int num = select(maxfd+1, &rdtemp, NULL, NULL, &timeout);// rdset中的数据被内核改写了, 只保留了发生变化的文件描述的标志位上的1, 没变化的改为0// 只要rdset中的fd对应的标志位为1 -> 缓冲区有数据了// 判断有无新连接// 有新连接if(FD_ISSET(lfd, &rdtemp)){// 接受连接请求, 这个调用不阻塞struct sockaddr_in cliaddr;int cliLen = sizeof(cliaddr);int cfd = accept(lfd, (struct sockaddr*)&cliaddr, (socklen_t*)&cliLen);printf("%d\n",cfd);// 得到了有效的文件描述符// 通信的文件描述符添加到读集合// 在下一轮select检测的时候, 就能得到缓冲区的状态FD_SET(cfd, &rdset);// 重置最大的文件描述符maxfd = cfd > maxfd ? cfd : maxfd;}// 没有新连接, 通信for(int i=0; i<maxfd+1; ++i){// 判断从监听的文件描述符之后到maxfd这个范围内的文件描述符是否读缓冲区有数据,i != lfd 是因为 lfd是监听的socketif(i != lfd && FD_ISSET(i, &rdtemp)){// 接收数据char buf[10] = {0};// 一次只能接收10个字节, 客户端一次发送100个字节// 一次是接收不完的, 文件描述符对应的读缓冲区中还有数据// 下一轮select检测的时候, 内核还会标记这个文件描述符缓冲区有数据 -> 再读一次// 	循环会一直持续, 直到缓冲区数据被读完为止int len = read(i, buf, sizeof(buf));if(len == 0){printf("客户端关闭了连接...\n");// 将检测的文件描述符从读集合中删除FD_CLR(i, &rdset);close(i);}else if(len > 0){// 收到了数据// 发送数据write(i, buf, strlen(buf)+1);printf("读取了数据回传给客户端\n");}else{// 异常perror("read");}}}}return 0;
}

select_client.cpp

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>int main()
{// 1. 创建用于通信的套接字int fd = socket(AF_INET, SOCK_STREAM, 0);if(fd == -1){perror("socket");exit(0);}// 2. 连接服务器struct sockaddr_in addr;addr.sin_family = AF_INET;     // ipv4addr.sin_port = htons(9999);   // 服务器监听的端口, 字节序应该是网络字节序inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));if(ret == -1){perror("connect");exit(0);}// 通信while(1){// 读数据char recvBuf[1024];// 写数据// sprintf(recvBuf, "data: %d\n", i++);fgets(recvBuf, sizeof(recvBuf), stdin);write(fd, recvBuf, strlen(recvBuf)+1);// 如果客户端没有发送数据, 默认阻塞read(fd, recvBuf, sizeof(recvBuf));printf("recv buf: %s\n", recvBuf);sleep(1);}// 释放资源close(fd); return 0;
}

客户端不需要使用IO多路转接进行处理,因为客户端和服务器的对应关系是 1:N,也就是说客户端是比较专一的,只能和一个连接成功的服务器通信。

虽然使用select这种IO多路转接技术可以降低系统开销,提高程序效率,但是它也有局限性:

  1. 待检测集合(第2、3、4个参数)需要频繁的在用户区和内核区之间进行数据的拷贝,效率低
  2. 内核对于select传递进来的待检测集合的检测方式是线性的
    • 如果集合内待检测的文件描述符很多,检测效率会比较低
    • 如果集合内待检测的文件描述符相对较少,检测效率会比较高
  3. 使用select能够检测的最大文件描述符个数有上限,默认是1024,这是在内核中被写死了的。

自己的一点理解:

select里面的timeout参数一定要设置为结构体,要么就设为NULL,NULL就是默认阻塞

调试可以发现服务端怎么工作的,select循环,只有当有空闲文件描述符的时候,分逻辑,到底是连接还是通信的。如果是通信的,会记录是否读完了数据,如果读完了,内核会将文件描述符重新标记为0,除非重新写入数据。

但是上面的客户端代码有个问题,就是服务端多次写入的时候,客户端只能收到前面10个字节的数据,但是如果循环调用read,客户端read会阻塞,因为这里服务端是不会主动发送数据的,只是把客户端传过来的数据重新传回去。想一下怎么解决这个问题?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/259623.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

rk3568 Android12音频问题

rk3568 Android12音频问题 前言 在调试客户的项目的时候&#xff0c;发现外置的音频功放在打开和关闭音频的时候会出现pop音&#xff0c;严重影响使用体验&#xff0c;好在最后解决了这个问题&#xff0c;遂记录一下解决过程。 tiny-alsa命令的使用 在调试ubuntu等一些纯li…

基于深度学习yolov5钢材瑕疵目标检测系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介简介YOLOv5钢材瑕疵目标检测系统特性1. 数据预处理2. 模型架构3. 训练策略4. 后处理 性能评估 二、功能三、系统四. 总结 一项目简介 # YOLOv5 钢材瑕疵目标…

什么是HTML?

✨前言✨ 本文主要介绍什么是HTML以及W3C &#x1f352;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f352;博主将持续更新学习记录收获&#xff0c;友友们有任何问题可以在评论区留言 文章目录 什么是HTMLHTML发展史HTML的特点什么…

微服务01

笔记&#xff1a; day03-微服务01 - 飞书云文档 (feishu.cn) 数据库连接不上&#xff1f; 要在虚拟机启动MySQL容器。docker start mysql 服务治理 服务提供者&#xff1a;暴露服务接口&#xff0c;供其他服务调用 服务消费者&#xff1a;调用其他服务提供的接口 注册中心&…

销售技巧培训之如何提高手机销售技巧

销售技巧培训之如何提高手机销售技巧 随着科技的迅速发展&#xff0c;手机已成为我们日常生活中不可或缺的一部分。作为一名手机销售员&#xff0c;了解手机销售技巧是必不可少的。本文将通过案例分析与实践&#xff0c;为你揭示手机销售的奥秘。 一、了解客户需求 在销售过程…

二百一十四、Linux——Linux系统时间比电脑时间慢5分钟

一、目的 服务器重启后&#xff0c;发现Linux的系统时间比电脑时间慢5分钟&#xff0c;于是看了些博客&#xff0c;终于找到了解决方法&#xff0c;记录一下&#xff0c;以防止后面出现同样的问题 二、问题 通过date查看&#xff0c;Linux系统时间比电脑时间慢5分钟 &#…

解决firefox(火狐)浏览器使用transform: scale导致的border不显示或显示不全的问题;

最近火狐遇到了此问题&#xff0c;查了许久没有解决办法也有说是因为火狐不支持小于1px单位的&#xff0c;也有说火狐浏览器本身的问题&#xff0c;然后也没有解决方案&#xff0c;最后没办法只能用最笨的方法解决。。。。 只针对Firefox使用CSS&#xff0c;使用’-moz-documen…

vue3移动端脚手架(纯净,集成丰富)

概述 一个纯净的移动端框架 &#xff0c;用到了 Vue3 vuex Vite3 Vant3 sass eslint stylelint htmlhint husky commitlint axios axios-adapter VConsole 自定义全局 loading &#xff0c;自定义函数式 dialog &#xff08;api模仿微信小程序&#xff09;&#x…

Amazon CodeWhisperer 开箱初体验

文章作者&#xff1a;Coder9527 科技的进步日新月异&#xff0c;正当人工智能发展如火如荼的时候&#xff0c;各大厂商在“解放”码农的道路上不断创造出各种 Coding 利器&#xff0c;今天在下就带大家开箱体验一个 Coding 利器&#xff1a; Amazon CodeWhisperer。 亚马逊云科…

DAP数据集成与算法模型如何结合使用

企业信息化建设会越来越完善&#xff0c;越来越体系化&#xff0c;当今数据时代背景下更加强调、重视数据的价值&#xff0c;以数据说话&#xff0c;通过数据为企业提升渠道转化率、改善企业产品、实现精准运营&#xff0c;为企业打造自助模式的数据分析成果&#xff0c;以数据…

C# WPF上位机开发(简易图像处理软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 图像处理是工业生产重要的环节。不管是定位、测量、检测还是识别&#xff0c;图像处理在工业生产中扮演重要的角色。而c#由于自身快速开发的特点&a…

红海云eHR 任意文件上传漏洞复现

0x01 产品简介 红海eHR是大中型企业广泛采用人力资源管理系统。红海云是国内顶尖的HR软件供应商,是新一代eHR系统的领导者。 0x02 漏洞概述 红海云EHR系统PtFjk.mob接口处存在未授权文件上传漏洞,攻击者可上传webshell来命令执行,获取服务器权限。 0x03 复现环境 FOFA:…