12.I/O复用

news2024/10/6 0:32:50

I/O复用

多进程方式跳过

基于I/O复用的服务器端

接下来讨论并发服务器实现方法的延伸。如果有读者已经跳过第10章和第11章,那就只需把本章内容当做并发服务器实现的第一种方法即可。将要讨论的内容中包含一部分与多进程服务器端的比较,跳过第10章和第11章的读者简单浏览即可。

多进程服务器端的缺点和解决方法

为了构建并发服务器、只要有客户端连接请求就会创建新进程。这的确是实际操作中采用的一种方案,但并非十全十美,因为创建进程时需要付出极大代价。这需要大量的运算和内存空间,由于每个进程都具有独立的内存空间,所以相互间的数据交换也要求采用相对复杂的方法(IPC 属于相对复杂的通信方法)。各位应该也感到需要IPC时会提高编程难度。

“那有何解决方案?能否在不创建进程的同时向多个客户端提供服务?”

当然能!本节讲解的I/O复用就是这种技术。大家听到有这种方法是否感到一阵兴奋?但请不要过于依赖该模型!该方案并不适用于所有情况,应当根据目标服务器端的特点采用不同实现方法。下面先理解"复用"(Multiplexing)的意义。

理解复用

“复用”在电子及通信工程领域很常见,向这些领域的专家询问其概念时,他们会亲切地进行如下说明:

"在1个通信频道中传递多个数据(信号)的技术。"能理解吗?不能的话就再看看"复用"的含义。

“为了提高物理设备的效率,用最少的物理要素传递最多数据时使用的技术。”

上述两种说法内容完全一致,只是描述方式有所区别。

复用技术在服务器端的应用

服务器端引入复用技术可以减少所需进程数。引入复用技术,可以减少进程数。重要的是,无论连接多少客户端,提供服务的进程只有1个。

关于I/O复用服务器端的另一种理解

某教室中有10名学生和1位教师,这些孩子并非等闲之辈,上课时不停地提问。学校没办法,只能给每个学生都配1位教师,也就是说教室中现有10位教师。此后,只要有新的转校生,就会增加1位教师,因为转校生也喜欢提问。这个故事中,如果把学生当作客户端,把教师当作与客户端进行数据交换的服务器端进程,则该教室的运营方式为多进程服务器端方式。
有一天,该校来了位具有超能力的教师。这位教师可以应对所有学生的提问,而且回答速度很快,不会让学生等待。因此,学校为了提高教师效率,将其他老师转移到了别的班。现在,学生提问前必须举手,教师确认举手学生的提问后再回答问题。也就是说,现在的教室以I/O复用方式运行。
虽然例子有些奇怪,但可以通过它理解I/O复用技法:教师必须确认有无举手学生,同样,I/O复用服务器端的进程需要确认举手(收到数据)的套接字,并通过举手的套接字接收数据。

理解 select函数并实现服务器端

运用select函数是最具代表性的实现复用服务器端方法。Windows平台下也有同名函数提供相同功能,因此具有良好的移植性。

select 函数的功能和调用顺序

使用select函数时可以将多个文件描述符集中到一起统一监视,项目如下。

  • 是否存在套接字接收数据?
  • 无需阻塞传输数据的套接字有哪些?
  • 哪些套接字发生了异常?

监视顽称为“事件”(event)

上述监视项称为"事件"。发生监视项对应情况时,称"发生了事件"。这是最常见的表达,希望各位熟悉。另外,本章不会使用术语"事件",而与本章密切相关的第17章将使用该术语,希望大家理解"事件"的含义,以及“发生事件”的意义。

select函数的使用方法与一般函数区别较大,更准确地说,它很难使用。但为了实现I/O复用服务器端,我们应掌握select函数,并运用到套接字编程中。认为"select函数是I/O复用的全部内容"也并不为过。接下来介绍select函数的调用方法和顺序。

  1. 步骤一
    1. 设置文件描述符
    2. 指定监视范围
    3. 设置超时
  2. 步骤二
    1. 调用select函数
  3. 步骤三
    1. 查看调用结果

可以看到,调用selet函数前需要一些准备工作,调用后还需查看结果。接下来按照上述顺序逐一讲解。

设置文件描述符

利用select函数可以同时监视多个文件描述符。当然,监视文件描述符可以视为监视套接字。此时首先需要将要监视的文件描述符集中到一起。集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述3种监视项分成3类。

使用fd_set数组变量执行此项操作,如图12-6所示。该数组是存有0和1的位数组。

在这里插入图片描述

图12-6中最左端的位表示文件描述符0(所在位置)。如果该位设置为1,则表示该文件描述符是监视对象。那么图中哪些文件描述符是监视对象呢?很明显,是文件描述符1和3。

是否应当通过文件描述符的数字直接将值注册到fd_set变量?

当然不是!针对fd_set变量的操作是以位为单位进行的,这也意味着直接操作该变量会比较繁琐。难道要求各位自己完成吗?实际上,在fd_set变量中注册或更改值的操作都由下列宏完成

  • FD_ZERO(fd_set *fdset):将fd_set变量的所有位初始化为0。
  • FD SET(int fd,fd_set *fdset):在参数fdset指向的变量中注册文件描述符fa的信息。
  • FD_CLR(int fd,fd_set *fdset): 从参数fdset指向的变量中清除文件描述符fd的信息。
  • FD_ISSET(int fd,fd_set *fdset):若参数fiset指向的变量中包含文件描述符d的信息,则返回“真”。

上述函数中,FD_ISSET用于验证select函数的调用结果。通过图12-7解释这些函数的功能,简洁易懂,无需赘述。

在这里插入图片描述

设置检查(监视)范围及超时

简单介绍select函数。

#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
// 成功时返回大于θ的值,失败时返回-1。

// maxfd            监视对象文件描述符数量。
// readset          将所有关注"是否存在待读取数据"的文件描述符注册到fd_set型变量,并传递其地址值。
// writeset         将所有关注"是否可传输无阻塞数据"的文件描述符注册到fd_set型变量,并传递其地址值。
// exceptset        将所有关注"是否发生异常"的文件描述符注册到fd_set型变量,并传递其地址值。
// timeout          调用select函数后,为防止陷入无限阻塞的状态,传递超时(time-out)信息。
// 返回值           发生错误时返回-1,超时返回时返回0。因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。

如上所述,select函数用来验证3种监视项的变化情况。根据监视项声明3个fd set型变量,分别向其注册文件描述符信息,并把变量的地址值传递到上述函数的第二到第四个参数。但在此之前(调用select函数前)需要决定下面2件事。

“文件描述符的监视(检查)范围是?”

“如何设定select函数的超时时间?”

第一,文件描述符的监视范围与select函数的第一个参数有关。实际上,select函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在fd_set变量中的文件描述符数。但每次新建文件描述符时,其值都会增1,故只需将最大的文件描述符值加1再传递到select函数即可。加1是因为文件描述符的值从0开始。

第二,select函数的超时时间与select函数的最后一个参数有关,其中timeval结构体定义如下。

struct timeval
{
    long tv_sec;            // seconds
    long tv_usec;           // microseconds
}

本来select函数只有在监视的文件描述符发生变化时才返回。如果未发生变化,就会进入阻塞状态。指定超时时间就是为了防止这种情况的发生。通过声明上述结构体变量,将秒数填人tv_sec成员,将毫秒数填入tv_usec成员,然后将结构体的地址值传递到select函数的最后一个参数。此时,即使文件描述符中未发生变化,只要过了指定时间,也可以从函数中返回。不过这种情况下,select函数返回0。因此,可以通过返回值了解返回原因。如果不想设置超时,则传递NULL 参数。

调用select函数后查看结果

虽未给出具体示例,但图12-5中的步骤一"select函数调用前的所有准备工作"已讲解完毕,同时也介绍了select函数。而函数调用后查看结果也同样重要。我们已讨论过select函数的返回值,如果返回大于0的整数,说明相应数量的文件描述符发生变化。

文件描述符的变化

文件描述符变化是指监视的文件描述符中发生了相应的监视事件。例如,通过 select 的第二个参数传递的集合中存在需要读数据的描述符时,就意味着文件描述符发生变化。

select函数返回正整数时,怎样获知哪些文件描述符发生了变化?向select函数的第二到第四个参数传递的fd set变量中将产生如图12-8所示变化,获知过程并不难。

在这里插入图片描述

由图12-8可知,select函数调用完成后,向其传递的fd_set变量中将发生变化。原来为1的所有位均变为0,但发生变化的文件描述符对应位除外。因此,可以认为值仍为1的位置上的文件描述符发生了变化。

也就是说,调用完函数后,值仍为1的描述符是发生了变化的。

select函数调用示例

下面通过示例把select函数所有知识点进行整合,希望各位通过如下示例完全理解之前的内容。

#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include <string>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>

constexpr size_t BUF_SIZE = 1024;

int main(int argc, char* argv[])
{
    fd_set reads;
    // 初始化reads
    FD_ZERO(&reads);
    // 监视描述符0,0代表标准输入,控制台输入
    FD_SET(0, &reads);

    timeval timeout;
    // 不能在这里设置时间
    // 因为调用select函数后,结构体timeval的成员tv_sec和tv_usec的值将被替换为超时前剩余时间。
    // 因此,调用select函数前,每次都需要初始化timeval结构体变量。
    // timeout.tv_sec = 5;
    // timeout.tv_usec = 0;

    char buf[BUF_SIZE] = { 0 };

    while (true) {
        // 将准备好的fd_set变量reads的内容复制到temps变量,因为之前讲过,调用select 函数后,除发生变化的文件描述符对应位外,剩下的所有位将初始化为0。
        // 因此,为了记住初始值,必须经过这种复制过程。
        // 这是使用select函数的通用方法,希望各位牢记。
        fd_set temps = reads;
        // 每次都需要初始化timeval结构体变量
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;
        // 第一个参数是1,因为监视的最后一个描述符是0,+1就是1
        // 第二个是reads的备份,后两个没有要监视的,所以传空,最后传入超时
        int res = select(1, &temps, nullptr, nullptr, &timeout);
        if (res == -1) {
            // 返回-1,说明出现错误
            std::cout << "select 错误" << std::endl;
            break;
        } else if (res == 0) {
            // 返回0,说明超时
            std::cout << "超时" << std::endl;
        } else {
            // 大于0代表发生事件的描述符个数,这里只有一个,所以直接判断
            if (FD_ISSET(0, &temps)) {
                int strLen = read(0, buf, BUF_SIZE);
                buf[strLen] = 0;
                std::cout << "select 事件数据:" << buf << std::endl;
            }
        }
    }

    return 0;
}


运行后若无任何输入,经5秒将发生超时。若通过键盘输入字符串,则可看到相同字符串输出。

实现I/O复用服务器端

下面通过select函数实现I/O复用服务器端。之前已给出关于select函数的所有说明,各位只需通过示例掌握利用select函数实现服务器端的方法。下列示例是基于I/O复用的回声服务器端

#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include <string>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

constexpr size_t BUF_SIZE = 1024;

int main(int argc, char* argv[])
{
    if (argc != 2) {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        return 0;
    }

    // 创建套接字
    int servSock = socket(PF_INET, SOCK_STREAM, 0);
    if (servSock == -1) {
        std::cout << "socket 错误" << std::endl;
        return 0;
    }
    // 初始化服务端地址信息
    sockaddr_in servAdr;
    std::memset(&servAdr, 0, sizeof(servAdr));
    servAdr.sin_family = AF_INET;
    servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAdr.sin_port = htons(std::atoi(argv[1]));
    // 绑定地址信息
    int stu = bind(servSock, (sockaddr*)&servAdr, sizeof(servAdr));
    if (stu == -1) {
        close(servSock);
        std::cout << "bind 错误" << std::endl;
        return 0;
    }
    // 进入监听状态
    stu = listen(servSock, 5);
    if (stu == -1) {
        close(servSock);
        std::cout << "listen 错误" << std::endl;
        return 0;
    }

    // 初始化fd_set
    fd_set reads;
    FD_ZERO(&reads);
    FD_SET(servSock, &reads);
    int fdMax = servSock;
    // 生命时间对象
    timeval timeout;

    char buf[BUF_SIZE] = { 0 };

    while (true) {
        fd_set cpyReads = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;

        int fdNum = select(fdMax + 1, &cpyReads, nullptr, nullptr, &timeout);
        if (fdNum == -1) {
            std::cout << "select 错误" << std::endl;
            break;
        }
        if (fdNum == 0) {
            std::cout << "超时" << std::endl;
            continue;
        }
        // 有fdNum个套接字产生了事件
        // 从0遍历到最大套接字,寻找变化了的套接字
        for (int sockId = 0; sockId <= fdMax; ++sockId) {
            if (!FD_ISSET(sockId, &cpyReads)) {
                continue;
            }
            if (sockId == servSock) {
                // 如果变化的套接字ID等于当前服务端套接字
                // 说明有新的客户端接入
                sockaddr_in clntAdr;
                socklen_t szAdr = sizeof(clntAdr);
                // 接收客户端请求
                int clntSock = accept(servSock, (sockaddr*)&clntAdr, &szAdr);
                // 监听客户端套接字
                FD_SET(clntSock, &reads);
                // 更新最大套接字序号
                if (fdMax < clntSock) {
                    fdMax = clntSock;
                }
                std::string adrStr = inet_ntoa(clntAdr.sin_addr);
                std::cout << "接收" << adrStr << "的请求:" << clntSock << std::endl;
            } else {
                // 接收对应套接字的数据
                int strLen = read(sockId, buf, BUF_SIZE);
                if (strLen == -1) {
                    // read错误
                    FD_CLR(sockId, &reads);
                    close(sockId);
                    std::cout << "套接字" << sockId << "读取错误" << std::endl;
                } else if (strLen == 0) {
                    // 请求已关闭
                    FD_CLR(sockId, &reads);
                    close(sockId);
                    std::cout << "套接字" << sockId << "已关闭" << std::endl;
                } else {
                    // 接收数据后,写给客户端
                    write(sockId, buf, strLen);
                }
            }
        }
    }

    close(servSock);

    return 0;
}


为了验证运行结果,我使用了第4章介绍的echo_client.c,其实上述回声服务器端也可与其他回声客户端配合运行。

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

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

相关文章

Android WebView中H5调用Android原生方法

最近做项目&#xff0c;使用webView看一些网页&#xff0c;和网页开发一起找什么方法进行交互&#xff0c;还好解决&#xff0c;分享一下经验。 对于webView的使用就不写了&#xff0c;百度大法好&#xff0c;主要是交互方面&#xff0c;对WebView增加以下代码&#xff1a; bi…

五个了解自己天赋优势的分析工具(一)霍兰德兴趣测试

霍兰德兴趣测试 霍兰德职业兴趣自测&#xff08;Self-Directed Search&#xff09;是由美国职业指导专家霍兰德&#xff08;John Holland&#xff09;根据他本人大量的职业咨询经验及其职业类型理论编制的测评工具。 霍兰德认为&#xff0c;个人职业兴趣特性与职业之间应有一…

74、Beyond RGB: Scene-Property Synthesis with Neural Radiance Fields

简介 List item 论文地址&#xff1a;http://arxiv-export3.library.cornell.edu/abs/2206.04669v1 利用隐式三维表示和神经渲染的最新进展&#xff0c;从综合模型的角度提供了一种新的场景理解方法&#xff0c;能够从新颖的视点渲染照片逼真的RGB图像&#xff0c;而且还能够…

我们怎样才能过好这一生?

文章目录1. 日拱一卒&#xff0c;功不唐捐1.1 适当的时候给自己一个奖励1.2 一个人可能走的更快&#xff0c;但一群人才能走的更远1.3 通过一些事情去逼自己一把1.4 从真理中去感悟1.5 当你面临绝路时2. 梦想的意义不在于实现3. 孤独4. 烦恼5. 别总说来日方长6. 忍和韧性7. 事情…

低成本搭建一台家庭存储服务器:前篇

本篇文章&#xff0c;记录搭建备份服务器的过程。 写在前面 今年考虑专门搭建一台用于数据备份的机器&#xff0c;一来今年外出的需求比较多&#xff0c;历史的设备已经用了几年了&#xff0c;需要有更新的设备来“接力”&#xff1b;二来也想验证方案的靠谱程度&#xff0c;…

k8s之ingress实战小栗子

写在前面 本文接k8s之ingress 。 本文看一个基于ingress作为流量入口的实战例子&#xff0c;架构图如下&#xff1a; 接下来详细看下。 1&#xff1a;部署MariaDB 首先我们需要定义MariaDB使用的configmap&#xff0c;如下&#xff1a; apiVersion: v1 kind: ConfigMap meta…

1587_AURIX_TC275_SMU的部分寄存器3

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) SMU的章节&#xff0c;剩下的部分全都是寄存器了&#xff0c;没有太多需要特别关注的。因此&#xff0c;接下来选择性整理&#xff0c;完成整个SMU的文档学习整理。 这一页是上一份笔记的…

05_FreeRTOS中断管理

目录 什么是中断 中断相关寄存器 源码实验 什么是中断 简介:让CPU打断正常运行的程序,转而去处理紧急的事件(程序) ,就叫中断。 举例:上课可以比做CPU正常运行的程序,上厕所可以比做中断程序。 中断执行机制,可简单概括为三步: 中断请求:外设产生中断请求(GPIO外部中断、…

【精品】k8s(Kubernetes)cka由基础到实战学法指南

轻松快速学会k8s四招 图1 k8s四招 学完本篇,您会获得什么惊喜? 从初学k8s,到帮助别人学会的过程中,发现朋友们和我,并非不努力,而是没有掌握更好的方法。有方法可让我们学的更快更轻松,这篇文章,以一个networkpolicy的题目,来逐步讲解,帮助大家建立一种,自己可以根…

Java基础语法

文章目录Java 基础语法一、注释1. 注释介绍2. 注释分类3. 注释颜色二、关键字1. 关键字介绍2. 所有关键词三、字面量四、变量1. 变量2. Debug 工具1&#xff09;如何加断点&#xff1f;2&#xff09;如何开启 Debug 运行&#xff1f;3&#xff09;点哪里 ?4&#xff09;看哪里…

ElasticSearch架构之整合ELK

前言本篇文章主要是说ElasticSearch对Logstash、FileBeat、Kibana整合形成ELK的架构&#xff0c;为什么需要整合这个架构呢&#xff1f;一个很重要的原因就是我们开发过程中有相当多的日志需要进行查看&#xff0c;如果我们要查找一个问题需要到多台服务器进行查看那是相当麻烦…

【Java基础知识 4】Java数据类型之间的转换、运算符

本文已收录专栏 &#x1f332;《Java进阶之路》&#x1f332; 目录 &#x1f334;基本数据类型 &#x1f343;01、布尔 &#x1f343;02、byte &#x1f343;03、short &#x1f343;04、int &#x1f343;05、long &#x1f343;06、float &#x1f343;07、double …

C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】

&#x1f451;作者主页&#xff1a;Fire_Cloud_1 &#x1f3e0;学习社区&#xff1a;烈火神盾 &#x1f517;专栏链接&#xff1a;万物之源——C 文章目录一、程序的翻译环境和执行环境二、详解编译链接1、前言小知识&#x1f50d;2、翻译环境【important】2.1 编译① 预编译【…

【LeetCode每日一题】【2023/1/15】2293. 极大极小游戏

文章目录2293. 极大极小游戏方法1&#xff1a;双指针2293. 极大极小游戏 LeetCode: 2293. 极大极小游戏 简单\color{#00AF9B}{简单}简单 给你一个下标从 0 开始的整数数组 nums &#xff0c;其长度是 2 的幂。 对 nums 执行下述算法&#xff1a; 设 n 等于 nums 的长度&#x…

Windows 10的子系统不是非Ubuntu不可

Ubuntu 的制造商 Canonical 早已和微软进行合作&#xff0c;让我们体验了极具争议的 Bash on Windows。外界对此也是褒贬不一&#xff0c;许多 Linux 重度用户则是质疑其是否有用&#xff0c;以及更进一步认为 Bash on Windows 是一个安全隐患。 Unix 的 Bash 是通过 WSL&#…

MyBatis动态设置表名 获取添加功能自增的主键 自定义映射

MyBatis动态设置表名 获取添加功能自增的主键 自定义映射动态设置表名获取添加功能自增的主键自定义映射解决字段名和属性名不一致的情况为字段起别名,保持和属性名的一致设置全局配置,保持和属性名的一致通过resultMap设置自定义的映射关系动态设置表名 mapper接口&#xff1a…

Java实现线段树

问题一&#xff1a;开始的子区间是怎么分的&#xff1f; M (LR)/2&#xff0c;左子区间为[L,M]&#xff0c;右子区间为[M1,R] 问题二&#xff1a;如何进行区间统计&#xff1f; 假设这13个数为1,2,3,4,1,2,3,4,1,2,3,4,1. 在区间之后标上该区间的数字之和&#xff1a; 如…

Windows安全加固-AD建立与加入

AD建立与加入 实验环境说明&#xff1a; 域控从机&#xff1a; Windows 2003 域控主机&#xff1a; Windows 2008 IP设置 设置同一网段-保证连通性 将NDS指向Windos2008 第一步&#xff0c;域控服务器需要将服务器设为静态 IP地址 1.将Windows2008服务器IP地址修改为192.1…

c++ - 第24节 - c++的IO流

1.C语言的输入与输出 C语言中我们用到的最频繁的输入输出方式就是scanf()与printf()。 scanf(): 从标准输入设备(键盘)读取数据&#xff0c;并将值存放在变量中。printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。注意宽度输出和精度输出控制。C语言借助了相应的缓冲区…

【Android gradle】自定义一个android gradle插件之buildSrc

文章目录1. 前言2. 简单使用3. 其余用法3.1 依赖&版本管理3.2 插件版本自增3.3 其余4. 链接1. 前言 在【Android gradle】自定义一个android gradle插件&#xff0c;并发布到私有Artifactory仓库这篇文章中介绍了定义一个gradle插件&#xff0c;然后发布到远程或者本地仓库…