当前位置: 首页 > news >正文

12.I/O复用

前言

本章将讨论并发服务器的第二种实现方法—基于I/O复用(Multi-plexing)的服务器端构建。虽然通过本章多学习一种服务器端实现方法非常重要,但更重要的是理解每种技术的优缺点。如果能掌握每种技术的优劣,就可以根据特定目标灵活应用不同模型,而不是仅关注功能实现。

一、基于1/O复用的服务器端

接下来讨论并发服务器实现方法的延伸。

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

为了构建并发服务器,只要有客户端连接请求就会创建新进程。这的确是实际操作中采用的一种方案,但并非十全十美,因为创建进程时需要付出极大代价。这需要大量的运算和内存空间,由于每个进程都具有独立的内存空间,所以相互间的数据交换也要求采用相对复杂的方法(IPC属于相对复杂的通信方法)。各位应该也感到需要IPC时会提高编程难度。
“那有何解决方案?能否在不创建进程的同时向多个客户端提供服务?”
当然能!本章节讲解的I/O复用就是这种技术。大家听到有这种方法是否感到一阵兴奋?但请不要过于依赖该模型!该方案并不适用于所有情况,应当根据目标服务器端的特点采用不同实现方法。下面先理解“复用”(Multiplexing)的意义(通信专业的应该很熟悉)。
“复用”在电子及通信工程领域很常见,向这些领域的专家询问其概念时,他们会亲切地进行如下说明:
“在1个通信频道中传递多个数据(信号)的技术。”
能理解吗?不能的话就再看看“复用”的含义。
“为了提高物理设备的效率,用最少的物理要素传递最多数据时使用的技术。”
上述两种说法内容完全一致,只是描述方式有所区别。下面我根据自己的理解进行解释。图中给出的是纸杯电话,相信大家上小学时也做过。
在这里插入图片描述
图是远距离的3人可以同时通话的3方对话纸杯电话系统。为使3人同时对话,需准备图中所示系统。另外,为了完成3人对话,说话时需同时对着两个纸杯,接听时也需要耳朵同时对准两个纸杯。此时引人复用技术会使通话更加方便,如下图所示。
在这里插入图片描述
我们上小学时做过类似系统(把线捆在中间并绷直)。构建这种系统就无需同时使用两个杯子,可以说小学就学过“复用”的概念了。接下来讨论复用技术的优点。
■ 减少连线长度。
■ 减少纸杯个数。
即使减少了连线和纸杯的量仍能进行3人通话,当然也有人在考虑如下这种情况:
“好像不能同时说话?”
实际上,因为是在进行对话,所以很少发生同时说话的情况。也就是说,上述系统采用的是“时(time)分复用技术”。而且,因为说话人声高(频率)不同,即使同时说话也能进行一定程度的区分(当然杂音也随之增多)。因此,也可以说系统同时采用了“频(frequency)分复用技术”。这样大家就能理解之前讲的“复用”的定义了。

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

纸杯电话系统引人复用技术后,可以减少纸杯数和连线长度。同样,服务器端引人复用技术可以减少所需进程数。为便于比较,先给出第10章的多进程服务器端模型,如图所示。
在这里插入图片描述
下图的模型中引人复用技术,可以减少进程数。重要的是,无论连接多少客户端,提供服务的进程只有1个。
在这里插入图片描述
以上就是I/O复用服务器端模型的讲解,下面考虑通过1个进程向多个客户端提供服务的方法。

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

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

1.select函数的功能和调用顺序

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

上述监视项称为“事件”。发生监视项对应情况时,称“发生了事件”。这是最常见的表达,希望大家熟悉。

select函数的使用方法与一般函数区别较大,更准确地说,它很难使用。但为了实现I/O复用服务器端,我们应掌握select函数,并运用到套接字编程中。认为“select函数是I/O复用的全部内容”也并不为过。接下来介绍select函数的调用方法和顺序,如图所示。
在这里插入图片描述
图中给出了从调用select函数到获取结果所经过程。可以看到,调用select函数前需要一些准备工作,调用后还需查看结果。接下来按照上述顺序逐一讲解。

2.设置文件描述符

利用select函数可以同时监视多个文件描述符。当然,监视文件描述符可以视为监视套接字。此时首先需要将要监视的文件描述符集中到一起。集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述3种监视项分成3类。
使用fd_set数组变量执行此项操作,如下图所示。该数组是存有0和1的位数组。
在这里插入图片描述
图中最左端的位表示文件描述符0(所在位置)。如果该位设置为1,则表示该文件描述符是监视对象。那么图中哪些文件描述符是监视对象呢?很明显,是文件描述符1和3。
“是否应当通过文件描述符的数字直接将值注册到fd_set变量?”
当然不是!针对fd_set变量的操作是以位为单位进行的,这也意味着直接操作该变量会比较繁琐。难道要求各位自己完成吗?实际上,在fd_set变量中注册或更改值的操作都由下列宏完成。
■ FD_ZERO(fd_setfdset):将fd_set变量的所有位初始化为0。
■ FD_SET(int fd,fd_set
fdset):在参数fdset指向的变量中注册文件描述符fd的信息。
■ FD_CLR(int fd,fd_setfdset):从参数fdset指向的变量中清除文件描述符fd的信息。
■ FD_ISSET(int fd,fd_set
fdset):若参数fdset指向的变量中包含文件描述符fd的信息,则返回“真”。
上述函数中,FD_ISSET用于验证select函数的调用结果。通过图解释这些函数的功能。
在这里插入图片描述

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

下面讲解- 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的值,该值是发生事件的文件描述符数。

大家看到这么多参数是不懵了…
如上所述,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; //secondslong tv_usec; //microseconds
}

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

4.调用select函数后查看结果

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


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

select函数返回正整数时,怎样获知哪些文件描述符发生了变化?向select函数的第二到第四个参数传递的fd_set变量中将产生如图所示变化,获知过程并不难。
在这里插入图片描述
由图可以看出,select函数调用完成后,向其传递的fd_set变量中将发生变化。原来为1的所有位均变为0,但发生变化的文件描述符对应位除外。因此,可以认为值仍为1的位置上的文件描述符发生了变化。

5.select函数调用示例

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

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30int 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); // 0 IS Standard input(consoLe)/*
timeout.tv_sec=5;
timeout.tv_usec=5000;
*/while(1)
{temps=reads;timeout.tv_sec=5;timeout.tv_usec=0;result=select(1,&temps,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;
}

第14、15行:看似复杂,实则简单。首先在第14行初始化fd_set变量,第15行将文件描述符0对应的位设置为1。换言之,需要监视标准输入的变化。
第24行:将准备好的fd_set变量reads的内容复制到temps变量,因为之前讲过,调用select函数后,除发生变化的文件描述符对应位外,剩下的所有位将初始化为0。因此,为了记住初始值,必须经过这种复制过程。这是使用select函数的通用方法,希望各位牢记。
第18、19行:请观察被注释的代码,这是为了设置select函数的超时而添加的。但不能在此时设置超时。因为调用select函数后,结构体timeval的成员tv_sec和tv_usec的值将被替换为超时前剩余时间。因此,调用select函数前,每次都需要初始化timeval结构体变量。
第25、26行:将初始化timeval结构体的代码插入循环后,每次调用select函数前都会初始化新值。
第27行:调用select函数。如果有控制台输入数据,则返回大于0的整数;如果没有输入数据而引发超时,则返回0。
第39~44行:select函数返回大于0的值时运行的区域。验证发生变化的文件描述符是否为标准输入。若是,则从标准输入读取数据并向控制台输出。
在这里插入图片描述
运行后若无任何输人,经5秒将发生超时。若通过键盘输入字符串,则可看到相同字符串输出。

6.实现1/0复用服务器端

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

#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_adr, clnt_adr;struct timeval timeout;fd_set reads, cpy_reads;socklen_t adr_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_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*) &serv_adr,sizeof(serv_adr))==-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)    // connection request!{adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);FD_SET(clnt_sock, &reads);if(fd_max<clnt_sock)fd_max=clnt_sock;printf("connected client: %d \n", clnt_sock);}else    // read message!{str_len=read(i, buf, BUF_SIZE);if(str_len==0) // close request!{FD_CLR(i, &reads);close(i);printf("CLOSED CLIENT: %d \n",i);}else{write(i, buf, str_len); // ecHo!}}}}}close(serv_sock);return 0;
}void error_handling(char * buf)
{fputs(buf, stderr);fputc('\n', stderr);exit(1);
}

第40行:向要传到select函数第二个参数的fd_set变量reads注册服务器端套接字。这样,接收数据情况的监视对象就包含了服务器端套接字。客户端的连接请求同样通过传输数据完成。因此,服务器端套接字中有接收的数据,就意味着有新的连接请求。
第49行:在while无限循环中调用select函数。select函数的第三和第四个参数为空。只需根据监视目的传递必要的参数。
第54、56行:select函数返回大于等于1的值时执行的循环。第56行调用FD_ISSET函数,查找发生状态变化的(有接收数据的套接字的)文件描述符。
第58、63行:发生状态变化时,首先验证服务器端套接字中是否有变化。如果是服务器端套接字的变化,将受理连接请求。特别需要注意的是,第63行在fd_set变量reads中注册了与客户端连接的套接字文件描述符。
第68行:发生变化的套接字并非服务器端套接字时,即有要接受的数据时执行else语句。但此时需要确认接收的数据是字符串还是代表断开连接的EOF。
第73、74行:接收的数据为EOF时需要关闭套接字,并从reads中删除相应信息。
第79行:接收的数据为字符串时,执行回声服务。

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


总结

本章的I/O复用技术在理论和实际中都非常重要,大家应熟练掌握哦。

http://www.dtcms.com/a/277320.html

相关文章:

  • 前端性能与可靠性工程:资源优化 - 加载性能的“低垂果实”
  • 从零开始学习深度学习-水果分类之PyQt5App
  • SpringBoot集成Redis、SpringCache
  • C++ 强制类型转换
  • 【操作系统】strace 跟踪系统调用(一)
  • (LeetCode 每日一题) 2410. 运动员和训练师的最大匹配数(排序、双指针)
  • es里为什么node和shard不是一对一的关系
  • Augment AI 0.502.0版本深度解析:Task、Guidelines、Memory三大核心功能实战指南
  • 将 NumPy 数组展平并转换为 Python 列表
  • 1.1.5 模块与包——AI教你学Django
  • OpenLayers 入门指南【二】:坐标系与投影转换
  • 把 DNA 当 PCIe:一条 365 nt 链实现 64 Gbps 片上光互连——基于链式 Förster 共振的分子级波分复用链路
  • 理解 Robots 协议:爬虫该遵守的“游戏规则”
  • MySQL逻辑删除与唯一索引冲突解决
  • M00224-小范围疫情防控元胞自动机模拟matlab
  • 【unitrix】 5.1 第二套类型级二进制数基本结构体(types2.rs)
  • 深入解析Hadoop架构设计:原理、组件与应用
  • OpenLayers使用
  • (2)从零开发 Chrome 插件:实现 API 登录与本地存储功能
  • 音视频学习(三十八):像素与位深
  • 打破并发瓶颈:虚拟线程实现详解与传统线程模型的性能对比
  • QuickUnion优化及Huffman树
  • JS红宝书pdf完整版
  • JAVA生成PDF(itextpdf)
  • 为什么玩游戏用UDP,看网页用TCP?
  • [2025CVPR]GNN-ViTCap:用于病理图像分类与描述模型
  • MyBatis框架进阶指南:深入理解CRUD与参数映射
  • Redis集群方案——哨兵机制
  • 无需付费即可利用AI消除音频噪声和生成字幕
  • 《Linux篇》自动化构建-make/Makefile