I/O复用函数的使用——select

news2025/4/22 6:40:05

I/O复用函数的使用——select

目录

一、概念

二、select接口

2.1 基础概念

 2.2  使用 select 函数的标准输入读取代码

 2.3 基于 `select` 模型的多客户端 TCP 服务器实现


一、概念

i/o复用使得程序能同时监听多个文件描述符,可以提高程序性能。

之前为了让服务器能够服务多个客户端,我们使用多线程或多进程进行服务器的并发,意味着有多少个客户端就要产生很多进程。会浪费,开销很大。所以我们引入i/o复用

i/o复用方法select,poll,epoll可以帮助应用程序找到就绪描述符eg:老师检查作业,谁写完了谁举手,老师过去检查,不用一个一个等待去检查

二、select接口

2.1 基础概念

检测键盘是否有数据输入,如果有打印输出,没有就阻塞。select系统调用的用途是在一段指定时间内,监听用户感兴趣的文件描述符的可读,可写异常事件。

系统调用原型

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:需要监视的文件描述符的最大值加1(fd + 1),因为文件描述符是从0开始的

  • readfdswritefdsexceptfds:分别表示可读、可写和异常条件的文件描述符集合,程序调用select函数时,通过这三个参数传入自己感兴趣的文件描述符。

  • timeout:超时时间,可以是NULL(阻塞等待),或者指定超时时间(非阻塞等待)。

  • 成功时,返回就绪文件描述符的总数,超时没有任何描述符就绪则返回0,失败返回-1,如果在等待期间,程序收到信号则立即返回-1并设置error为EINTR  

  • fd_set:结构体,fd_mask,数组类型,fd_bits,数组名。用于存储和管理一组文件描述符  fd_set可以容纳1024个位  数组收集  描述符,把数组中的描述符添加到集合fd_set

  • fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位bit标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量  

  • 由于位操作过于频繁,可以用一系列宏来访问fd_set结构体中的位

 

使用I/O复用技术(如selectpollepoll)来处理多个文件描述符(FD)的流程图

假设只有3和7上有数据,结构体集合经过select返回值为2,之后会测试和集合作比较看看对于的位是不是1,找到看哪个上面有数据

 2.2  使用 select 函数的标准输入读取代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <time.h>

#define STDIN  0 // 定义STDIN常量,表示标准输入的文件描述符

int main()
{
    int fd = STDIN; // 将文件描述符fd初始化为标准输入
    fd_set fdset;   // 定义一个文件描述符集合fdset

    while (1) // 无限循环,持续等待输入
    {
        FD_ZERO(&fdset); // 清空文件描述符集合fdset
        FD_SET(fd, &fdset); // 将标准输入的文件描述符添加到集合fdset中

        struct timeval tv = {5, 0}; // 定义一个timeval结构体,设置超时时间为5秒

        int n = select(fd+1, &fdset, NULL, NULL, &tv); // 调用select函数,监视fdset集合中的文件描述符
        if (-1 == n) // 如果select返回-1,表示发生错误
        {
            printf("select err\n"); // 打印错误信息
            continue; // 继续下一次循环
        }
        else if (n == 0) // 如果select返回0,表示超时,没有文件描述符就绪
        {
            printf("time out\n"); // 打印超时信息
        }
        else // 如果select返回大于0的值,表示有文件描述符就绪
        {
            if (FD_ISSET(fd, &fdset)) // 检查标准输入的文件描述符是否就绪
            {
                char buff[128] = {0}; // 定义一个字符数组buff,用于存储读取的数据
                read(fd, buff, 127); // 从标准输入读取数据到buff中,最多读取127个字符
                printf("read:%s\n", buff); // 打印读取到的数据
            }
        }
    }
    
    return 0; // 程序正常结束
}

 

这段代码使用select函数来监视标准输入(键盘输入),并在有输入时读取数据。它首先定义了一个文件描述符集合fdset,并将标准输入的文件描述符添加到这个集合中。然后,它调用select函数来监视这个集合中的文件描述符,等待最多5秒。如果在这5秒内有任何文件描述符就绪(即有输入),select函数会返回就绪的文件描述符的数量。程序然后检查标准输入的文件描述符是否就绪,并从标准输入读取数据。如果5秒内没有文件描述符就绪,select函数会返回0,程序会打印超时信息。如果select函数返回-1,表示发生错误,程序会打印错误信息并继续下一次循环。

  if (FD_ISSET(fd, &fdset)) 

FD_ISSET 是一个宏,用于检查一个特定的文件描述符(FD)是否已经包含在文件描述符集合(fd_set)中。这个宏是在使用 select 函数后,用来确定哪些文件描述符已经准备好进行 I/O 操作(如读、写)的常用方法。

struct timeval tv = {5, 0}; // 定义一个timeval结构体,设置超时时间为5秒

  • struct timeval tv = {5, 0};:定义了一个timeval结构体变量tv,并初始化它。

  • timeval结构体:在Linux中,timeval结构体用于表示时间值,它包含两个字段:

    • tv_sec:表示时间的秒数部分。

    • tv_usec:表示时间的微秒数部分。

  • 初始化:这里将tv_sec初始化为5,tv_usec初始化为0。这意味着超时时间为5秒。 

 2.3 基于 `select` 模型的多客户端 TCP 服务器实现

select.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define MAXFD 10 // 定义最大文件描述符数量

// 初始化文件描述符数组
void fds_init(int fds[])
{
    for (int i = 0; i < MAXFD; i++)
    {
        fds[i] = -1; // 将所有元素初始化为-1,表示未使用
    }
}

// 向文件描述符数组添加一个文件描述符
void fds_add(int fds[], int fd)
{
    for (int i = 0; i < MAXFD; i++)
    {
        if (-1 == fds[i])
        {
            fds[i] = fd; // 找到空位,添加文件描述符
            break; // 添加成功后退出循环
        }
    }
}

// 从文件描述符数组中删除一个文件描述符
void fds_del(int fds[], int fd)
{
    for (int i = 0; i < MAXFD; i++)
    {
        if (fds[i] == fd)
        {
            fds[i] = -1; // 找到文件描述符,将其删除
            break; // 删除成功后退出循环
        }
    }
}

// 初始化套接字
int socket_init()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个IPv4 TCP套接字
    if (sockfd == -1)
    {
        return -1; // 创建失败,返回-1
    }

    struct sockaddr_in saddr; // 定义一个sockaddr_in结构体,用于存储服务器地址信息
    memset(&saddr, 0, sizeof(saddr)); // 将结构体清零
    saddr.sin_family = AF_INET; // 设置地址族为IPv4
    saddr.sin_port = htons(6000); // 设置端口号为6000,并转换为网络字节序
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置IP地址为127.0.0.1

    int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)); // 绑定套接字到本地地址
    if (-1 == res)
    {
        printf("bind err\n"); // 绑定失败,打印错误信息
        return -1; // 返回-1
    }

    res = listen(sockfd, 5); // 开始监听连接请求,最多允许5个连接请求排队
    if (res == -1)
    {
        return -1; // 监听失败,返回-1
    }

    return sockfd; // 返回监听套接字的文件描述符
}

// 接受客户端连接请求
void accept_client(int sockfd, int fds[])
{
    int c = accept(sockfd, NULL, NULL); // 接受一个连接请求
    if (c < 0)
    {
        return; // 接受失败,直接返回
    }

    printf("accept c=%d\n", c); // 打印接受的客户端文件描述符
    fds_add(fds, c); // 将客户端文件描述符添加到数组
}

// 接收数据
void recv_data(int c, int fds[])
{
    char buff[128] = {0}; // 定义一个缓冲区,用于存储接收的数据
    int n = recv(c, buff, 127, 0); // 从客户端接收数据
    if (n <= 0)
    {
        close(c); // 关闭套接字
        fds_del(fds, c); // 从数组中删除客户端文件描述符
        printf("client close\n"); // 打印客户端关闭信息
        return; // 返回
    }

    printf("recv:%s\n", buff); // 打印接收到的数据
    send(c, "ok", 2, 0); // 向客户端发送确认信息
}

int main()
{
    int fds[MAXFD]; // 定义一个文件描述符数组
    fds_init(fds); // 初始化文件描述符数组

    int sockfd = socket_init(); // 初始化套接字
    if (sockfd == -1)
    {
        exit(1); // 初始化失败,退出程序
    }

    fds_add(fds, sockfd); // 将监听套接字的文件描述符添加到数组

    fd_set fdset; // 定义一个文件描述符集合
    while (1)
    {
        FD_ZERO(&fdset); // 清空文件描述符集合
        int maxfd = -1; // 初始化最大文件描述符
        for (int i = 0; i < MAXFD; i++)
        {
            if (fds[i] == -1)
            {
                continue; // 跳过未使用的文件描述符
            }

            if (maxfd < fds[i])
            {
                maxfd = fds[i]; // 更新最大文件描述符
            }

            FD_SET(fds[i], &fdset); // 将文件描述符添加到集合
        }

        struct timeval tv = {5, 0}; // 设置超时时间为5秒

        int n = select(maxfd + 1, &fdset, NULL, NULL, &tv); // 调用select函数,监视文件描述符集合
        if (-1 == n)
        {
            printf("select err\n"); // select失败,打印错误信息
        }
        else if (0 == n)
        {
            printf("time out\n"); // 超时,打印超时信息
        }
        else
        {
            for (int i = 0; i < MAXFD; i++)
            {
                if (-1 == fds[i])
                {
                    continue; // 跳过未使用的文件描述符
                }

                if (FD_ISSET(fds[i], &fdset)) // 检查文件描述符是否有就绪事件
                {
                    // 处理就绪的文件描述符
                    if (fds[i] == sockfd)
                    {
                        // 监听套接字就绪,接受客户端连接请求
                        accept_client(sockfd, fds);
                    }
                    else
                    {
                        // 客户端套接字就绪,接收数据
                        recv_data(fds[i], fds);
                    }
                }
            }
        }
    }
}

1. **包含必要的头文件**:
   - 引入标准输入输出、标准库、Unix 系统调用、字符串操作、套接字编程、IP 地址转换等相关的头文件。

2. **定义常量**:
   - 定义 `MAXFD` 常量,表示文件描述符数组的最大长度。

3. **初始化文件描述符数组** (`fds_init` 函数):
   - 遍历数组,将所有元素初始化为 `-1`,表示这些文件描述符当前未被使用。

4. **添加文件描述符到数组** (`fds_add` 函数):
   - 在数组中查找第一个 `-1` 值的位置,并将传入的文件描述符 `fd` 存储在该位置。

5. **从数组中删除文件描述符** (`fds_del` 函数):
   - 在数组中查找传入的文件描述符 `fd`,并将对应的元素设置为 `-1`。

6. **初始化套接字** (`socket_init` 函数):
   - 创建 TCP 套接字。
   - 设置服务器地址结构体 `sockaddr_in`,包括 IP 地址和端口号。
   - 绑定套接字到指定的 IP 地址和端口号。
   - 使套接字开始监听连接请求。

7. **接受客户端连接请求** (`accept_client` 函数):
   - 使用 `accept` 函数接受一个新的客户端连接请求。
   - 打印客户端文件描述符。
   - 将客户端文件描述符添加到文件描述符数组中。

8. **接收数据** (`recv_data` 函数):
   - 从指定的客户端文件描述符接收数据。
   - 如果接收到的数据长度小于等于0(表示客户端已关闭连接),则关闭套接字并从数组中删除对应的文件描述符。
   - 打印接收到的数据,并给客户端发送确认信息。

9. **主函数 (`main`)**:
   - 初始化文件描述符数组。
   - 调用 `socket_init` 函数初始化套接字,并将监听套接字的文件描述符添加到数组中。
   - 进入一个无限循环,持续等待和处理事件:
     - 清空 `fd_set` 结构体 `fdset`。
     - 遍历文件描述符数组,将所有有效的文件描述符添加到 `fdset` 中,并更新 `maxfd` 变量。
     - 设置超时时间。
     - 调用 `select` 函数监视 `fdset` 中的文件描述符,等待最多5秒钟。
     - 根据 `select` 的返回值处理错误、超时或就绪的文件描述符:
       - 如果 `select` 返回 `-1`,表示发生错误,打印错误信息。
       - 如果 `select` 返回 `0`,表示超时,打印超时信息。
       - 如果 `select` 返回大于 `0` 的值,表示有文件描述符就绪,遍历文件描述符数组,检查哪些文件描述符有就绪事件,并进行相应的处理:
         - 如果就绪的文件描述符是监听套接字,则调用 `accept_client` 函数接受新的客户端连接请求。
         - 如果就绪的文件描述符是客户端套接字,则调用 `recv_data` 函数接收数据。

通过这些步骤,服务器能够同时处理多个客户端连接请求和数据接收,实现高效的 I/O 复用。

这段代码实现了一个简单的TCP服务器,它使用 select 函数来同时处理多个客户端连接请求和数据接收。服务器首先初始化一个监听套接字,并将其文件描述符添加到文件描述符数组中。然后,它进入一个无限循环,使用 select 函数监视文件描述符数组中的所有文件描述符。当有文件描述符就绪时,服务器会根据文件描述符的类型(监听套接字或客户端套接字)来处理相应的事件(接受连接请求或接收数据)。 

cli.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个IPv4 TCP套接字
    if (sockfd == -1) // 检查套接字创建是否成功
    {
        exit(1); // 如果失败,退出程序
    }

    struct sockaddr_in saddr; // 定义一个sockaddr_in结构体,用于存储服务器地址信息
    memset(&saddr, 0, sizeof(saddr)); // 将结构体清零
    saddr.sin_family = AF_INET; // 设置地址族为IPv4
    saddr.sin_port = htons(6000); // 设置端口号为6000,并转换为网络字节序
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置IP地址为127.0.0.1

    int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 连接到服务器
    if (res == -1) // 检查连接是否成功
    {
        printf("connect err\n"); // 如果失败,打印错误信息
        exit(1); // 退出程序
    }

    while (1) // 无限循环,持续发送和接收数据
    {
        printf("input\n"); // 提示用户输入
        char buff[128] = {0}; // 定义一个缓冲区,用于存储输入和接收的数据
        fgets(buff, 128, stdin); // 从标准输入读取一行数据

        if (strncmp(buff, "end", 3) == 0) // 检查是否输入了"end"命令
        {
            break; // 如果是,退出循环
        }

        send(sockfd, buff, strlen(buff) - 1, 0); // 发送数据到服务器,不包括换行符
        memset(buff, 0, 128); // 清空缓冲区
        recv(sockfd, buff, 127, 0); // 从服务器接收数据
        printf("read:%s\n", buff); // 打印接收到的数据
    }

    close(sockfd); // 关闭套接字

    exit(0); // 程序正常结束
}

 这段代码实现了一个简单的TCP客户端,它连接到本地服务器(127.0.0.1:6000),并持续发送和接收数据。用户可以在命令行中输入数据,输入"end"命令时,程序会退出。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图像预处理-图像轮廓特征查找

其实就是外接轮廓&#xff0c;有了轮廓点就可以找到最上、最下、最左、最右的四个坐标&#xff08;因为有xmin,xmax,ymin,ymax&#xff09;。就可以绘制出矩形。 一.外接矩形 cv.boundingRect(轮廓点) - 返回x,y,w,h&#xff0c;传入一个轮廓的轮廓点&#xff0c;若有多个轮廓需…

全同态加密医疗数据分析集python实现

目录 摘要一、前言二、全同态加密与医疗数据分析概述2.1 全同态加密(FHE)简介2.2 医疗数据分析需求三、数据生成与预处理四、系统架构与流程4.1 系统架构图五、核心数学公式六、异步任务调度与(可选)GPU 加速七、PyQt6 GUI 设计八、完整代码实现九、自查测试与总结十、展望…

list的学习

list的介绍 list文档的介绍 list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。list的底层是双向链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向其前一个元素和后一…

HarmonyOS:Navigation实现导航之页面设置和路由操作

导读 设置标题栏模式设置菜单栏设置工具栏路由操作页面跳转页面返回页面替换页面删除移动页面参数获取路由拦截 子页面页面显示类型页面生命周期页面监听和查询 页面转场关闭转场自定义转场共享元素转场 跨包动态路由系统路由表自定义路由表 示例代码 Navigation组件适用于模块…

管道位移自动化监测方案

一、背景 管道系统在区域性地质沉降作用下易形成非均匀应力场集中现象&#xff0c;诱发管体屈曲变形及环焊缝界面剥离等连续损伤累积效应&#xff0c;进而导致管道力学性能退化与临界承载能力衰减。传统人工巡检受限于空间覆盖度不足及数据采集周期长&#xff08;≥72h&#xf…

【学习笔记】机器学习(Machine Learning) | 第五周| 分类与逻辑回归

机器学习&#xff08;Machine Learning&#xff09; 简要声明 基于吴恩达教授(Andrew Ng)课程视频 BiliBili课程资源 文章目录 机器学习&#xff08;Machine Learning&#xff09;简要声明 一、逻辑回归的基本原理分类判断条件模型输出的解释Sigmoid 函数与 Logistic 函数逻辑…

Python 深度学习 第8章 计算机视觉中的深度学习 - 卷积神经网络使用实例

Python 深度学习 第8章 计算机视觉中的深度学习 - 卷积神经网络使用实例 内容概要 第8章深入探讨了计算机视觉中的深度学习&#xff0c;特别是卷积神经网络&#xff08;convnets&#xff09;的应用。本章详细介绍了卷积层和池化层的工作原理、数据增强技术、预训练模型的特征…

[免费]SpringBoot+Vue博物馆(预约)管理系统【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的SpringBootVue博物馆(预约)管理系统&#xff0c;分享下哈。 项目视频演示 【免费】SpringBootVue博物馆(预约)管理系统 Java毕业设计_哔哩哔哩_bilibili 项目介绍 随着计算机科学技术的日渐成熟&#xff…

【python】pyCharm常用快捷键使用-(2)

pyCharm常用快捷键使用 快速导入任意类 【CTRLALTSPACE】代码补全【CTRLSHIFTENTER】代码快速修正【ALTENTER】代码调试快捷键

机器视觉lcd屏增光片贴合应用

在现代显示制造领域&#xff0c;LCD屏增光片贴合工艺堪称显示效果的"画龙点睛"之笔。作为提升屏幕亮度、均匀度和色彩表现的关键光学组件&#xff0c;增光片的贴合精度直接影响着终端用户的视觉体验。传统人工贴合方式难以满足当前超窄边框、高分辨率显示屏的严苛要求…

VScode-py环境

settings.json {"git.ignoreLimitWarning": true,"code-runner.runInTerminal": true,"code-runner.executorMap": {"python": "python3"} } 第二句话保证在终端里面进行IO 第三句话保证python3的用户不会执行python关键…

用键盘实现控制小球上下移动——java的事件控制

本文分享Java的一个有趣小项目&#xff0c;实现用键盘控制小球的移动 涉及java知识点&#xff1a;Swing GUI框架&#xff0c;绘图机制&#xff0c;事件处理&#xff0c;焦点控制 1.编写窗口和面板 (1.)定义面板类 Panel 继承自Java 自带类JPanel (2.)定义窗口类 window 继承…

《马尼拉》桌游期望计算器

《马尼拉》桌游期望计算器&#xff1a;做出最明智的决策 注&#xff1a;本项目仍在开发验证中&#xff0c;计算结果可能不够准确&#xff0c;欢迎游戏爱好者提供协助&#xff01; 在线使用 | GitHub 项目简介 马尼拉期望计算器是一个基于 Vue 3 Vite 开发的网页应用&#xff…

动态LOD策略细节层级控制:根据视角距离动态简化远距量子态渲染

动态LOD策略在量子计算可视化中的优化实现 1. 细节层级控制:动态简化远距量子态渲染 在量子计算的可视化中,量子态通常表现为高维数据(如布洛赫球面或多量子比特纠缠态)。动态LOD(Level of Detail)策略通过以下方式优化渲染性能: 距离驱动的几何简化: 远距离渲染:当…

线程池的介绍

目录 一、什么是线程池 二、线程池的详细内容 三、线程池的简化 一、什么是线程池 提到线程池&#xff0c;我们可能想到 常量池&#xff0c;可以先来说说常量池&#xff1a; 像是字符串常量&#xff0c;在Java程序最初构建的时候&#xff0c;就已经准备好了&#xff0c;等程…

安恒安全渗透面试题

《网安面试指南》https://mp.weixin.qq.com/s/RIVYDmxI9g_TgGrpbdDKtA?token1860256701&langzh_CN 5000篇网安资料库https://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247486065&idx2&snb30ade8200e842743339d428f414475e&chksmc0e4732df793fa3bf39…

计算机是如何工作的(上)

对于学习JavaEE初阶为什么要知道计算机是如何工作的&#xff0c;是因为在未来我们写代码的时候&#xff0c;会出现一些bug&#xff0c;而在代码层面是看不出来的&#xff0c;所以我们需要了解一些关于计算机内部是如何工作的&#xff0c;从而提高代码的健壮度。 计算机的组成&…

基础服务系列-Windows10 安装AnacondaJupyter

下载 https://www.anaconda.com/products/individual 安装 安装Jupyter 完成安装 启动Jupyter 浏览器访问 默认浏览器打开&#xff0c;IE不兼容&#xff0c;可以换个浏览器 修改密码 运行脚本

Kubernetes架构介绍

实验环境 安装好k8s集群 一、kubernetes组件构成 1、架构图 2、组件介绍 使用以下命令查看相关资源 kubectl get nodes 查看群集节点 kubectl get ns 查看名称空间 kubectl get pod -A …

远程服务器的mysql连接不上,问题出在哪里

使用本地ideal测试连接报错记录 排查 检查mysql服务是否正常,输入命令systemctl status mysql查看 检查端口netstat -plnt | grep mysql 最后检查服务器的防火墙设置 我以为在服务器厂商的控制面板设置放行规则就行&#xff0c;导致一直无法排查出问题&#xff0c;最后才发现由…