TCP/IP网络编程 第十二章:I/O复用

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

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

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


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

理解复用

在网络编程中,复用(Multiplexing)是指在一个物理通信链接(如网络传输介质)上同时传输多个独立的数据流。它通过将多个数据流合并成一个流并在接收端将其分解,从而提高网络资源的利用效率。

复用技术可以通过以下几种方式实现:

  1. 时间复用(Time Division Multiplexing,TDM):将时间划分为若干个间隔,每个间隔分配给不同的数据流进行传输。发送端按照一定的规则在每个时间间隔内发送数据,接收端则根据间隔进行数据的提取和恢复。

  2. 频分复用(Frequency Division Multiplexing,FDM):将频率范围划分为多个窄带信道,每个信道专门用于传输一个数据流。数据流经过调制后,在不同的频率上进行传输,接收端则对信号进行解调得到原始数据。

  3. 码分复用(Code Division Multiplexing,CDM):利用不同的码序列来区分各个数据流。发送端使用特定的码序列对数据进行扩展,接收端则使用相同的码序列进行解扩,从而将数据流分离。

以上这些复用技术都旨在实现多个数据流在同一物理通信链接上的传输,以提高网络的带宽利用率和传输效率。在网络编程中,我们可以使用不同的复用技术来实现同时处理多个客户端请求或在单个连接上传输多个数据流。

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

对于网络编程中的IO复用(IO multiplexing),它是一种高效处理多个IO事件的机制。传统的IO模型中,每个IO操作都会阻塞线程,导致程序在处理一个IO时无法同时处理其他IO事件,造成资源浪费。

而IO复用通过利用特定的系统调用函数(如select、poll、epoll等)来监视多个IO事件,将多个IO操作集中在一个线程中进行管理和处理,从而实现同时处理多个IO事件的能力。它的基本原理是将需要监听的IO事件加入到一个事件集合中,然后通过系统调用阻塞等待其中任何一个事件就绪,一旦有就绪事件,程序就可以执行相应的操作。

IO复用的主要好处包括:

  1. 资源利用率高:使用IO复用可以避免每个IO操作都阻塞线程,从而减少线程数量,提高了资源利用效率。

  2. 响应速度快:IO复用可以同时监听多个IO事件,一旦有事件就绪,立即进行处理,大大减小了事件响应的延迟。

  3. 编程简洁:相比于多线程或多进程模型,使用IO复用可以简化代码,降低开发和维护的难度。

总而言之,IO复用是一种高效处理多个IO事件的机制,它可以减少线程数量、提高资源利用率和响应速度,是网络编程中常用的技术之一。

我再举个例子来理解一下IO复用服务器端,某教室中有10名学生和1位教师,这些孩子并非等闲之辈,上课时不停地提问。学校没办法,只能给每个学生都配1位教师,也就是说教室中现有10位教师。此后,只要有新的转校生,就会增加1位教师,因为转校生也喜欢提问。这个故事中,如果把学生当作客户端,把教师当作与客户端进行数据交换的服务器端进程,则该教室的运营方式为多进程服务器端方式。


有一天,该校来了位具有超能力的教师。这位教师可以应对所有学生的提问,而且回答速度很快,不会让学生等待。因此,学校为了提高教师效率,将其他老师转移到了别的班。现在,学生提问前必须举手,教师确认举手学生的提问后再回答问题。也就是说,现在的教室以IO复用方式运行。
虽然例子有些奇怪,但可以通过它理解IO复用方法:教师必须确认有无举手学生,同样,IO复用服务器端的进程需要确认举手(收到数据)的套接字,并通过举手的套接字接收数据。

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

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

select函数的功能和调用顺序

使用sclect函数时可以将多个文件描述符集中到一起统一监视,项目如下。
□是否存在套接字接收数据?
□无需阻塞传输数据的套接字有哪些?
□哪些套接字发生了异常?

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

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

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

设置文件描述符

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


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

 图中最左端的位表示文件描述符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指向的变量中注册文件描述符的信息。

□ FD_CLR(int fd, fd_set *fdset):从参数fdset指向的变量中清除文件描述符的信息。

□ FD_ISSET(int fd,fd_set *fdset):若参数fdset指向的变量中包含文件描述符的信息,则返回“真”。
上述函数中,FD_ISSET用于验证select函数的调用结果。

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

先简单的介绍一下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);//成功时返回大于0的值,失败时返回-1
      maxfd     //监视对象文件描述符数量。
      readset   //将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量,并传递其地址值。 
      writeset  //将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量,并传递其地址值。  
      exceptset //将所有关注“是否发生异常”的文件描述符注册到fd_set型变量,并传递其地址值。
      timeout   //调用select函数后,为防止陷入无限阻塞的状态,传递超时(time-out)信息。
      返回值    //发生错误时返回-1,超时返回时返回0。因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数

如上所述,selcct函数用来验证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函数后查看结果

函数调用后查看结果也同样重要。我们已经讨论过select函数的返回值,如果返回了大于0的整数,说明相应数量的文件描述符发生变化。那么这个变化是如何变化的呢?select函数调用完成过后,向其传递的fd_set变量中将发生变化。原来为1的所有位均变为0,但发生变化的文件描述符对应位除外。因此,可以认为值仍为1的位置上的文件描述符发生了变化。

select函数调用示例

#include<stdio.h>
#include<unistd.h>
#include<sys/time.h>
#include<sys/select.h>
#define BUF_SIZE 30

int main(int argc,char *argv){
    fd_set reads,temps;
    int result,str_len;
    char buf[BUF_SIZE];
    struct timeval timeout;

    FD_ZERO(&reads);
    FD_SET(0,&reads);

    while(1){
       temps=reads;
       timeout.tv_sec=5;
       timeout.tv_usec=0;
       result=select(1,&temp,0,0,&timeout);
       if(result==-1){
            puts("select() error!");
            break;
       }
       else if(result==0){
            puts("Time-out!");
       }
       else{
            if(FD_ISSET(0,&temps)){
               str_len=read(0,buf,BUF_SIZE);
               buf[str_len]=0;
               printf("message from console: %s",buf);
            }
       }
    }
return 0;
}

上述内容有两个点要注意:

1.由于select函数调用后会修改监视fd_set中的内容,所以我们需要保存一份副本,上述代码保存在temps中。

2.由于select函数调用时,timeout中的时间最后会被替换为超时前剩余时间,所以这个timeout初始量也需要每次初始化。

实现IO复用服务器端

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

#define BUF_SIZE 100
void error_handling(char *buf);

int main(int argc,char *argv[]){
    int serv_sock,clnt_sock;
    struct sockaddr_in serv_addr,clnt_addr;
    struct timeval timeout;
    fd_set reads,cpy_reads;

    socklen_t addr_sz;
    int fd_max,str_len,fd_num,i;
    char buf[BUF_SIZE];
    if(argc!=2){
         printf("Usage: %s <port>\n",argv[0]);
         exit(1);
    }

    serv_sock=socket(PF_INET,SOCK_STREAM,0);
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi([argv[1]]));

    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
         error_handling("bind() error");
    if(listen(serv_sock,5)==-1)
         error_handling("listen() error");

    FD_ZERO(&reads);
    FD_SET(serv_sock,&reads);
    fd_max=serv_sock;

    while(1){
         cpy_reads=reads;
         timeout.tv_sec=5;
         timeout.tv_usec=5000;

         if((fd_num=select(fd_max+1,&cpy_reads,0,0,&timeout))==-1)
              break;
         if(fd_num==0)
              continue;

         for(i=0;i<fd_max+1;++i){
              if(FD_ISSET(i,&cpy_reads)){     
                     if(i==serv_sock){     //连接请求到来
                         addr_sz=sizeof(clnt_addr);
                         clnt_sock=accept(serv_sock,(structsockaddr*)&clnt_addr,addr_sz);
                         FD_SET(clnt_sock,&reads);
                         if(fd_max<clnt_sock)fd=clnt_sock;
                         printf("connected client: %d \n",clnt_sock);
                     }
                     else{
                         str_len=rad(i,buf,BUF_SIZE);
                         if(str_len==0){
                               FD_CLR(i,&reads);
                               close(i);
                               printf("closed client: %d \n",i);
                         }
                         else{
                             write(i,buf,str_len);//回声
                         }
                     }
              }
         }
    }
    close(serv_sock);
    return 0;
}

void error_handling(char *buf){
    fputs(buf,stderr);
    fputc('\n',stderr);
    exit(1);
}

基于Windows的实现

在Windows平台调用select函数

Windows同样提供select函数,而且所有参数与Linux的select函数完全相同。只不过Window平台select函数的第一个参数是为了保持与(包括Linux的)UNIX系列操作系统的兼容性而添加的,并没有特殊意义。

#include <winsock2.h>
int select(int nfds, fd_set *treadfds, fd_set *writefds, fd_set *excepfds, const struct
timeval * timeout);//成功时返回0,失败时返回-1。

返回值、参数的顺序及含义与之前的Linux中的select函数全相同,故省略。下面给出timeval
结构体定义。

typedef struct timeval{
      long tv_sec;
      long tv_usec;
} TIMEVAL;

可以看到,基本结构与之前Linux中的定义相同,但Windows中使用的是typedef声明。接下来观察fd_set结构体。Windows中实现时需要注意的地方就在于此。可以看到,Windows的fd_set并非像Linux中那样采用了位数组。

typedef struct fd_set{
    u_int fd_count;
    SOCKET fd_array[FD_SETSIZE];
} fd_set;

Windows的fd_set由成员fd_count和fd_array构成,fd_count用于接字句柄数,fd_array用于保存套接字句柄,只要略加思考就能理解这样声明的原因。Linux的文件描述符从0开始递增,因此可以找出当前文件描述符数量和最后生成的文件描述符之间的关系。但Windows的套接字句柄并非从0开始,而且句柄的整数值之间并无规律可循,因此需要直接保存句柄的数组和记录句柄数的变量。幸好处理fd_set结构体的FDXXX型的4个宏的名称、功能及使用方法与Linux完全相同(故省略),这也许是微软为了保证兼容性所做的考量。

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

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

相关文章

智慧校园能源管控系统

智慧校园能源管控系统是一种搭载了物联网技术、大数据技术、大数据等技术性智能化能源管理方法系统&#xff0c;致力于为学校提供更高效、安全性、可信赖的能源供应管理和服务。该系统包括了校内的电力工程、水、气、暖等各类能源&#xff0c;根据对能源的实时检测、数据统计分…

uni-app中a标签下载文件跳转后左上角默认返回键无法继续返回

1.首先使用的是onBackPress //跟onShow同级别 onBackPress(option){ uni.switchTab({ url:/pages/....... return true }) }发现其在uni默认头部中使用是可以的 但是h5使用了"navigationStyle":"custom"后手机默认的返回并不可以&#xff0c; 2.经过查询…

【MySQL技术专题】「问题实战系列」深入探索和分析MySQL数据库的数据备份和恢复实战开发指南(备份+恢复篇)

深入探索和分析MySQL数据库的数据备份和恢复实战开发指南 MySQL数据库备份全量备份全量备份应用场景 增量备份binlogbinlog主要作用binlog的作用主要有两个方面 开启binlog日志功能要开启MySQL的binlog日志步骤 mysqlbinlogmysqlbinlog的使用案例 全量备份与增量备份结合按天全…

WebRTC不同方案对比

1.功能上会有一些出入&#xff0c;尤其是国内的metaRTC版本迭代很快&#xff0c; 2.后续的ffmpeg也在进行支持webrtc特性&#xff0c;obs新的版本好像已经支持了webrtc&#xff0c; 3.对于webrtc部分缺少的信令部分的标准化也有了对应的标准whip和whep协议 所以&#xff0c;如…

好的CRM需要有哪些特点?

CRM客户管理系统在企业中占有举足轻重的地位&#xff0c;既是战略工具又可以强化部门间的团队协作、优化销售流程、缩短销售周期。市面上crm做得比较好的公司有哪些&#xff1f; 1.上榜Gartner魔力象限 好的CRM市场的引领、产品研发的持续投入、技术创新以及不断增长的市场份…

性能测试:Jmeter-Beanshell请求加密实例

目录 1. 打包加密方法Jar包&#xff0c;导入Jmeter 2. 加密参数 总结&#xff1a; 进行性能测试时&#xff0c;有可能遇到一种场景&#xff1a;接口请求由于安全问题&#xff0c;需要进行加密发送。 这种场景下&#xff0c;使用Jmeter实现性能测试&#xff0c;则也需要使用…

自学网络安全究竟该从何学起?

一、为什么选择网络安全&#xff1f; 这几年随着我国《国家网络空间安全战略》《网络安全法》《网络安全等级保护2.0》等一系列政策/法规/标准的持续落地&#xff0c;网络安全行业地位、薪资随之水涨船高。 未来3-5年&#xff0c;是安全行业的黄金发展期&#xff0c;提前踏入行…

Redis基本全局命令(含key过期策略)

Redis基本全局命令 KEYEXISTSDELEXPIRETTLRedis的key过期策略TYPE KEY 返回所有满⾜样式&#xff08;pattern&#xff09;的key。⽀持如下统配样式。 h?llo 匹配 hello , hallo 和 hxlloh*llo 匹配 hllo 和 heeeelloh[ae]llo 匹配 hello 和 hallo 但不匹配 hilloh[^e]llo 匹配…

使用Pandas计算两个系统客户名称的相似度

引言&#xff1a; 在日常业务处理中&#xff0c;我们经常会面临将不同系统中的数据进行匹配和比对的情况。特别是在涉及到客户管理的领域&#xff0c;我们需要确保两个系统中的客户记录是准确、一致和无重复的。 本文将介绍如何使用Python的Pandas库来处理这个问题。我们将以…

美颜SDK与动态贴纸技术的发展趋势:向更智能、更新颖的美化

美颜SDK和动态贴纸技术在近年来迅速发展&#xff0c;成为移动应用、社交媒体和视频直播等领域中不可或缺的元素。本文将探讨美颜SDK和动态贴纸技术的最新发展趋势&#xff0c;包括智能化算法的应用、增强现实的融合以及个性化定制的兴起。我们将展望未来&#xff0c;展示这些技…

STM32(HAL库)通过ADC读取MQ2数据

目录 1、简介 2、CubeMX初始化配置 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 ADC外设配置 2.3 串口外设配置 2.4 项目生成 3、KEIL端程序整合 3.1 串口重映射 3.2 ADC数据采集 3.3 主函数代 3.4 效果展示 1、简介 本文通过STM32F103C8T6单片机通过HAL库方式对M…

注释气泡图函数(更新)

之前我们写过一个原创可视化函数Dotplot_anno.R&#xff0c;nature级别图表&#xff1a;一个注释气泡热图函数&#xff08;适用于单细胞及普通数据&#xff09;。主要解决的问题是1) 单细胞基因可视化分组注释。2) Bulk RNA差异基因热图、气泡图。3) 富集分析结果气泡图展示。这…

【案例实战】高并发业务的多级缓存架构一致性解决方案

我们在高并发的项目中基本上都离不开缓存&#xff0c;那么既然引入缓存&#xff0c;那就会有一个缓存与数据库数据一致性的问题。 首先&#xff0c;我们先来看看高并发项目里面Redis常见的三种缓存读写模式。 Cache Aside 读写分离模式&#xff0c;是最常见的Redis缓存模式&a…

JVM理论(五)执行引擎--解释器/JIT编译器

概述 首先执行引擎是java虚拟机核心的组成部分之一;而JVM的主要任务是装载字节码到内存,但不能够直接运行在操作系统之上.因为字节码指令并非等价于本地机器指令,它仅仅包含能够被JVM所识别的指令、符号表、以及其他信息;而此时执行引擎就华丽登场,它的任务就是将字节码指令解…

Appium Android ——利用 TestNG 并行执行用例

目录 前言&#xff1a; 一、测试类 二、连接两个 Android 设备或启动两个虚拟机 三、项目路径下新建两个 testng.xml 四、开启两个 appium server 五、导出依赖 六、执行测试 七、查看报告 前言&#xff1a; Appium是一个流行的移动应用自动化测试工具&#xff0c;…

当DevOps遇到AI,黑马迎来3.0时代丨IDCF

随着GhatGPT的爆火&#xff0c;人工智能和研发效能&#xff0c;无疑成为了2023的两个最重要的关键词。大规模语言模型LLM和相关应用的快速发展正在对研发团队的工作方式产生深远影响&#xff0c;这几乎象征着新的生产力革命的到来。 那么&#xff0c;作为一名工程师&#xff0…

拓宽“主航道”的Serverless与EDA领域,亚马逊云科技不断创新开拓

在新潮如走马灯般变换的时尚界&#xff0c;每隔几年就会刮起一阵复古风。被誉为“时尚教父”的著名设计师安德烈莱昂塔利曾说&#xff1a;“时尚总是在寻找新的灵感和方向&#xff0c;而复古是其中一个重要的来源。” 无独有偶。日新月异的高科技领域也会出现公认的“过时”…

【Ajax】笔记-取消请求

在进行AJAX(Asynchronous JavaScript and XML) 请求时&#xff0c;有时候我们需要取消正在进行的请求。取消请求可以帮助我们提高用户体验&#xff0c;病减少不必要的网络流量和服务器负载。 取消请求的方法 在AJAX请求中&#xff0c;我们可以使用以下方法来取消正在进行的请求…

【C语言】扫雷----详解(扩展版)

&#x1f341; 博客主页:江池俊的博客_CSDN博客 &#x1f341; 如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏&#x1f31f; 三连支持一下博主&#x1f49e; ✉️每一次努力都是一次进步&#xff0c;每一次尝试都是一次机会。无论遇到什么困难&#xff0c;…

使用Python和Scrapy实现抓取网站数据

Scrapy是一个功能强大的网络爬虫框架&#xff0c;允许开发者轻松地抓取和解析网站内容&#xff0c;这篇文章主要为大家介绍了如何使用Python的Scrapy库进行网站数据抓取&#xff0c;需要的可以参考一下 在本文中&#xff0c;我们将介绍如何使用Python的Scrapy库进行网站数据抓…