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. 创建监听的fd
    int 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是监听的socket
            if(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;     // ipv4
    addr.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.mfbz.cn/a/230185.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

内网环境下 - 安装linux命令、搭建docker以及安装镜像

一 内网环境安装docker 先在外网环境下载好docker二进制文件docker二进制文件下载&#xff0c;要下载对应硬件平台的文件&#xff0c;否则不兼容 如下载linux平台下的文件&#xff0c;直接访问这里即可linux版本docker二进制文件 这里下载docker-24.0.5.tgz 将下载好的文件…

爬虫 selenium语法 (八)

目录 一、为什么使用selenium 二、selenium语法——元素定位 1.根据 id 找到对象 2.根据标签属性的属性值找到对象 3.根据Xpath语句获取对象 4.根据标签名获取对象 5.使用bs语法获取对象 6.通过链接文本获取对象 三、selenium语法——访问元素信息 1.获取属性的属性值…

深度学习疲劳检测 驾驶行为检测 - python opencv cnn 计算机竞赛

文章目录 0 前言1 课题背景2 相关技术2.1 Dlib人脸识别库2.2 疲劳检测算法2.3 YOLOV5算法 3 效果展示3.1 眨眼3.2 打哈欠3.3 使用手机检测3.4 抽烟检测3.5 喝水检测 4 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习加…

[ndss 2023]确保联邦敏感主题分类免受中毒攻击

Securing Federated Sensitive Topic Classification against Poisoning Attacks 摘要 我们提出了一种基于联邦学习 (FL) 的解决方案&#xff0c;用于构建能够检测包含敏感内容的 URL 的分布式分类器&#xff0c;即与健康、政治信仰、性取向等类别相关的内容。尽管这样的分类器…

力扣题:数字与字符串间转换-12.9

力扣题-12.9 [力扣刷题攻略] Re&#xff1a;从零开始的力扣刷题生活 力扣题1&#xff1a;412. Fizz Buzz 解题思想&#xff1a;直接遍历添加至answer即可 class Solution(object):def fizzBuzz(self, n):""":type n: int:rtype: List[str]"""…

kubesphere安装后启用DevOps

官方文档&#xff1a;KubeSphere DevOps 系统 1、集群管理---定制资源定义 进入目录&#xff1a;集群管理---定制资源定义搜索&#xff1a;clusterconfiguration 点击 ks-installer 右侧的 &#xff0c;选择编辑 YAML 在该 YAML 文件中&#xff0c;搜索 devops&#xff0c;…

利用法线贴图渲染逼真的3D老虎模型

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 当谈到游戏角色的3D模型风格时&#xff0c;有几种不同的风格&#xf…

SpringBoot项目访问resources下的静态资源

1.新建一个配置文件夹&#xff0c;放配置类 2.编辑 WebMvcConfig.java package com.southwind.configuration;import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import or…

Hazel引擎学习(十二)

我自己维护引擎的github地址在这里&#xff0c;里面加了不少注释&#xff0c;有需要的可以看看 参考视频链接在这里 Scene类重构 参考&#xff1a;《InsideUE4》GamePlay架构&#xff08;二&#xff09;Level和World 目前我的Scene类基本只是给entt的封装&#xff0c;提供了…

做数据分析为何要学统计学(5)——什么问题适合使用t检验?

t检验&#xff08;Students t test&#xff09;&#xff0c;主要依靠总体正态分布的小样本&#xff08;例如n < 30&#xff09;对总体均值水平进行差异性判断。 t检验要求样本不能超过两组&#xff0c;且每组样本总体服从正态分布&#xff08;对于三组以上样本的&#xff0…

基于深度学习yolov5实现安全帽人体识别工地安全识别系统-反光衣识别系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 实现安全帽人体识别工地安全识别系统需要使用深度学习技术&#xff0c;特别是YOLOv5算法。下面是对基于YOLOv5实现安…

Jenkins部署python接口自动化测试

一、点击新建Item 二、指定源码和分支 私钥位置&#xff1a;C:\Users\Administrator\.ssh 文件下 三、构建脚本编写 四、构建后操作 指定输出的allure 结果目录 总结&#xff1a; 感谢每一个认真阅读我文章的人&#xff01;&#xff01;&#xff01; 作为一位过来人也是希望…

增强现实中的真实人/机/环与虚拟人/机/环

在增强现实中&#xff0c;真实人与虚拟人、真实机器与虚拟机器、真实环境与虚拟环境之间有着密切的关系。增强现实技术通过将真实与虚拟相结合&#xff0c;打破了传统的现实世界与虚拟世界的界限&#xff0c;创造出了一种新的体验方式。真实人、真实机器和真实环境与其对应的虚…

数据分析基础之《matplotlib(5)—直方图》

一、直方图介绍 1、什么是直方图 直方图&#xff0c;形状类似柱状图却有着与柱状图完全不同的含义。直方图牵涉统计学的概念&#xff0c;首先要对数据进行分组&#xff0c;然后统计每个分组内数据元的数量。在坐标系中&#xff0c;横轴标出每个组的端点&#xff0c;纵轴表示频…

SugarCRM 任意文件上传漏洞复现(CVE-2023-22952)

0x01 产品简介 SugarCRM是美国SugarCRM公司的一套开源的客户关系管理系统(CRM)。该系统支持对不同的客户需求进行差异化营销、管理和分配销售线索,实现销售代表的信息共享和追踪。 0x02 漏洞概述 SugarCRM index.php接口存在安全漏洞,该漏洞源于安装组件中存在授权绕过和P…

spark链接hive时踩的坑

使用spark操作hive&#xff0c;使用metastore连接hive&#xff0c;获取hive的数据库时&#xff0c;当我们在spark中创建数据库的时候&#xff0c;创建成功。 同时hive中也可以看到这个数据库&#xff0c;建表插入数据也没有问题&#xff0c;但是当我们去查询数据库中的数据时&a…

Springboot+AOP+注解实现字段AES+Base64加解密

AOP实现AESBASE64加解密 场景如下&#xff1a; 需要对数据库存储的字段&#xff0c;进行加解密的处理。如果都直接写代码的话&#xff0c;那么代码回冗余很多&#xff0c;所以使用AOP注解去实现。让代码简洁&#xff0c;方便 具体实现如下&#xff1a; 1、依赖 <depende…

微信小程序 bindtap 事件多参数传递

在微信小程序中&#xff0c;我们无法直接通过 bindtap"handleClick(1,2,3)" 的方式传递参数&#xff0c;而是需要通过自定义属性 data- 的方式进行传递&#xff0c;并在事件回调函数中通过 event.currentTarget.dataset 来获取这些参数。然而&#xff0c;这种传参方式…

SpringAOP专栏二《原理篇》

上一篇SpringAOP专栏一《使用教程篇》-CSDN博客介绍了SpringAop如何使用&#xff0c;这一篇文章就会介绍Spring AOP 的底层实现原理&#xff0c;并通过源代码解析来详细阐述其实现过程。 前言 Spring AOP 的实现原理是基于动态代理和字节码操作的。不了解动态代理和字节码操作…

1、关于前端js-ajax绕过

1、Ajax知识 、js--Ajax 传统请求跟js--Ajax请求的差别 在实例中用的上js-ajax的有 表单验证&#xff1a; 在用户填写表单时&#xff0c;可以使用 Ajax 在不刷新页面的情况下验证表单字段&#xff0c;并提供即时反馈。 实时搜索&#xff1a; 在搜索框中输入内容时&#xff0…
最新文章