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的值,失败时返回-1maxfd     //监视对象文件描述符数量。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;   //secondslong 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 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);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完全相同(故省略),这也许是微软为了保证兼容性所做的考量。

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

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

相关文章

Unity Arduino 串口通信

一、Unity端发送消息&#xff0c;Arduino端接收消息 通过串口通信 Arduino端 #include <Arduino.h>#define PIN_KEY 5 uint item;void setup() {item 0;Serial.begin(115200);pinMode(PIN_KEY, OUTPUT); }void loop() {if(Serial.available()>0){item Serial.rea…

同比环比数据可视化

引言 数据分析和可视化在现代商业环境中变得越来越重要。随着数据的迅速增长&#xff0c;我们需要有效的工具来解释和理解这些数据。 数据可视化提供了一种直观的方式&#xff0c;帮助我们从海量数据中提取有意义的见解&#xff0c;以支持业务决策。 同比环比图作为一种常见的…

ceph集群(二)

ceph 一、资源池 Pool 管理二、创建 CephFS 文件系统 MDS 接口三、创建 Ceph 块存储系统 RBD 接口四、创建 Ceph 对象存储系统 RGW 接口五、OSD 故障模拟与恢复 一、资源池 Pool 管理 上次我们已经完成了 Ceph 集群的部署&#xff0c;但是我们如何向 Ceph 中存储数据呢&#x…

Nginx 解析漏洞复现

Nginx 解析漏洞复现 一、环境搭建二、漏洞原理三、漏洞复现 一、环境搭建 如下介绍kali搭建的教程 cd ~/vulhub/nginx/nginx_parsing_vulnerability // 进入指定环境 docker-compose up -d // 启动环境docker-compose ps使用这条命令查看当前正在运行的环境 访问http://y…

MFC第十八天 非模式对话框、对话框颜色管理、记事本项目(查找替换、文字和背景色、Goto(转到)功能的开发)

文章目录 非模式对话框非模式对话框的特点非模式对话框与QQ聊天窗口开发非模态对话框&#xff08;Modeless Dialog&#xff09;和模态对话框&#xff08;Modal Dialog&#xff09;区别 记事本开发CFindReplaceDialog类的成员查找替换(算法分析)使用RichEdit控件 开发Goto(转到)…

[LINUX]之字符串去掉前后空格

去掉字符串前后空格通过使用awk $1$1命令实现 echo " test " | awk $1$1

STM32实现MLX90614非接触测温串口显示(标准库与HAL库实现)

目录 模块选择 编程环境 MLX90614基本原理 通信协议&#xff08;SMBus通信&#xff0c;类IIC通信&#xff09; 代码实现 STM32与模块之间接线表 1.标准库实现温度采集 2.HAL库实现温度采集 模块选择 STM32F103C8T6 MLX90614 非接触式红外测温传感器 编程环境 KEIL5&…

图数据库:neo4j学习笔记

参考资料&#xff1a;neo4j 教程_w3cschool Springboot集成Neo4j_喝醉的咕咕鸟的博客-CSDN博客 SpringBoot 整合 Neo4j_springboot neo4j_$懒小猿$的博客-CSDN博客 图数据库Neo4j实战&#xff08;全网最详细教程&#xff09;_neo4j使用教程_星川皆无恙的博客-CSDN博客 代码片段…

04 QT坐标系

在QT中默认左上角为原点&#xff0c;即&#xff08;0,0&#xff09;点。x轴右侧为正方向&#xff0c;y轴以下侧为正方向

nosql作业

nosql作业 文章目录 作业一&#xff1a;string list hash结构中&#xff0c;每个至少完成5个命令&#xff0c;包含插入 修改 删除 查询&#xff0c;list 和hash还需要增加遍历的操作命令1、 string类型数据的命令操作&#xff1a;2、 list类型数据的命令操作&#xff1a;3、 ha…

.NET网络编程——TCP通信

一、网络编程的基本概念 : 1. 网络 就是将不同区域的电脑连接到一起&#xff0c;组成局域网、城域网或广域网。把分部在不同地理区域的计算机于专门的外部设备用通信线路 互联成一个规模大、功能强的网络系统&#xff0c;从而使众多的计算机可以方便地互相传递信息&#xff0c…

坐标系变换的坑

坐标系变换的坑 坐标系变换本来是很简单的事情&#xff0c;公式也很简单。但是卡了我很多天&#xff0c;原因是&#xff1a;两个坐标系的位姿&#xff0c;虽然都是右手系&#xff0c;但我的在顺时针旋转是yaw角是递增的&#xff0c;同事发给我的却是逆时针递减的。 理论上很简…

Jenkins+Robot 接口自动化测试

目录 前言&#xff1a; 设计目标 项目说明 目录结构 配置 jenkins 1.安装插件 2.配置项目 前言&#xff1a; JenkinsRobot是一种常见的接口自动化测试方案&#xff0c;可以实现自动化的接口测试和持续集成。Jenkins是一个流行的持续集成工具&#xff0c;而Robot Framew…

55 # 实现可写流

先在 LinkedList.js 给链表添加一个移除方法 class Node {constructor(element, next) {this.element element;this.next next;} }class LinkedList {constructor() {this.head null; // 链表的头this.size 0; // 链表长度}// 可以直接在尾部添加内容&#xff0c;或者根据…

聊聊ChatGPT是如何组织对话的

为什么要组织对话&#xff1f; 总所周知&#xff0c;ChatGPT的训练大致可分为下图中展示的几个阶段&#xff0c;其中&#xff0c;在Pretraining阶段&#xff0c;模型的训练数据是纯文本&#xff0c;目标是根据上文预测下一个token&#xff0c;而在后面的几个阶段中&#xff0c…

网络安全能力成熟度模型介绍

一、概述 经过多年网络安全工作&#xff0c;一直缺乏网络安全的整体视角&#xff0c;网络安全的全貌到底是什么&#xff0c;一直挺迷惑的。目前网络安全的分类和厂家非常多&#xff0c;而且每年还会冒出来不少新的产品。但这些产品感觉还是像盲人摸象&#xff0c;只看到网络安…

回归预测 | MATLAB实现WOA-CNN鲸鱼算法优化卷积神经网络的数据多输入单输出回归预测

回归预测 | MATLAB实现WOA-CNN鲸鱼算法优化卷积神经网络的数据多输入单输出回归预测 目录 回归预测 | MATLAB实现WOA-CNN鲸鱼算法优化卷积神经网络的数据多输入单输出回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 回归预测 | MATLAB实现WOA-CNN鲸鱼算法优化卷积…

数据采集专家----4通道AD采集子卡推荐

FMC136是一款4通道250MHz采样率16位AD采集FMC子卡&#xff0c;符合VITA57规范&#xff0c;可以作为一个理想的IO模块耦合至FPGA前端&#xff0c;4通道AD通过高带宽的FMC连接器&#xff08;HPC&#xff09;连接至FPGA从而大大降低了系统信号延迟。 该板卡支持板上可编程采样时钟…

Linux进程

Linux进程 对于进程的理解&#xff0c;我们要从计算机的重要的冯诺依曼体系结构讲起&#xff0c;只有知道我们的程序/文件是如何在计算机中被操作运行并输出到显示器中&#xff0c;通过对于操作系统的理解&#xff0c;才能对于进程进行一定的理解。 文章目录 Linux进程冯诺依…

c#示例-json序列化和json树

序列化 由于指针和引用类型的存在&#xff0c;在运行中的程序中&#xff0c;数据不一定是整块的。 可能东一块西一块散落在内存的各个地方。 序列&#xff0c;是指连续且有序的一个整体。序列化就是把数据变为连续有序整体的过程。 经过这样处理后的数据就可以方便的进行传输…