IO多路复用

news/2024/6/20 17:38:12

IO多路复用


什么是IO多用复路

IO多路复用(Input/Output Multiplexing)是一种在单个线程中管理多个输入/输出通道的技术。它允许一个线程同时监听多个输入流(例如网络套接字、文件描述符等),并在有数据可读或可写时进行相应的处理,而不需要为每个通道创建一个独立的线程。== 没有文件句柄就绪时会阻塞应用程序,交出cpu。多路是指网络连接,复用指的是同一个线程

为什么有IO多路复用机制?

没有IO多路复用机制时,有BIO、NIO两种实现方式,但有一些问题

同步阻塞(BIO)

服务端采用单线程,当accept一个请求后,在recv或send调用阻塞时,将无法accept其他请求(必须等上一个请求处recv或send完),无法处理并发

服务器端采用多线程,当accept一个请求后,开启线程进行recv,可以完成并发处理,但随着请求数增加需要增加系统线程,大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000个线程真正发生读写事件的线程数不会超过20%,每次accept都开一个线程也是一种资源浪费

详细请看上一章服务器的调度策略

TCP协议 - 惠hiuj - 博客园

同步非阻塞(NIO)

服务器端当accept一个请求后,加入fds集合,每次轮询一遍fds集合recv(非阻塞)数据,没有数据则立即返回错误,每次轮询所有fd(包括没有发生读写事件的fd)会很浪费cpu

Untitled

IO多用复路

服务器端采用单线程通过select/epoll等系统调用获取fd列表,遍历有事件的fd进行accept/recv/send,使其能支持更多的并发连接请求


select函数接口的使用

select Linux内核监测这些套接字状态,读状态,写状态
检测的文件描述符并不是客户端socket的文件描述符
而是连接成功后服务器的accept的文件描述符
accept 的返回值,是服务器的内核维护的
服务器的应用程序并不知道,是那个客户端发送的
只有Linux内核知道,recv是将内核缓冲区拷贝到用户程序的buf中
读就绪状态:可以读
未就绪状态
写就绪状态:封完包状态,放在写的内核缓冲区
内核会判断,内核缓冲区的读写状态,状态达成
才会解除 read/write 的堵塞状态
一直用select监听状态的变化
当select返回值时,会清空集合没有处于就绪态的套接字,只保留需要重新,将文件描述符,加入集合

https://img2023.cnblogs.com/blog/2703082/202406/2703082-20240611011546824-2001402192.png

如果需要监听的数量很多,可以将文件的描述符,的集合存储在(在创建集合前将文件放到)数据结构中,然后遍历数据结构,检测是否还留到集合中,对文件做处理

套接字监听的数量是固定的(1024)


#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数说明

  • nfds: 监控的文件描述符范围是从 0 到 nfds-1。通常设置为所监控的文件描述符中最大值加 1。
  • readfds: 一个 fd_set 结构体,用于监控可读的文件描述符集合。如果不需要监控读操作,可以传入 NULL
  • writefds: 一个 fd_set 结构体,用于监控可写的文件描述符集合。如果不需要监控写操作,可以传入 NULL
  • exceptfds: 一个 fd_set 结构体,用于监控异常条件(如带外数据)的文件描述符集合。如果不需要监控异常情况,可以传入 NULL
  • timeout: 一个 timeval 结构体,指定 select 函数的超时时间。如果传入 NULL,则 select 将永远阻塞,直到某个文件描述符准备就绪。如果设置超时时间为 0,则 select 会立即返回。

返回值

  • 返回值为正数:表示准备就绪的文件描述符数量。
  • 返回值为 0:表示在指定的超时时间内没有文件描述符准备就绪。
  • 返回值为 -1:表示出现错误,同时设置 errno 以指示错误类型。

fd_set 的操作

fd_set 是一个位集合,用于存储文件描述符。常用的宏和函数包括:

  • FD_ZERO(fd_set *set): 清空集合。
  • FD_SET(int fd, fd_set *set): 将文件描述符 fd 加入集合。
  • FD_CLR(int fd, fd_set *set): 将文件描述符 fd 从集合中移除。
  • FD_ISSET(int fd, fd_set *set): 检查文件描述符 fd 是否在集合中。

select缺点

  • 单个进程所打开的FD是有限制的,通过FD_SETSIZE设置,默认1024
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

poll函数接口

poll与select相比,只是没有fd的限制,其它基本一样

poll 函数是另一个用于监控多个文件描述符的系统调用,与 select 类似,但在某些方面更灵活和高效。poll 函数的接口在 C 语言中的定义如下:


#include <poll.h>
// 数据结构
struct pollfd {int fd;                         // 需要监视的文件描述符short events;                   // 需要内核监视的事件short revents;                  // 实际发生的事件
};// API
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

参数说明

  • fds: 一个指向 pollfd 结构体数组的指针,每个 pollfd 结构体描述一个要监控的文件描述符及其事件。
  • nfdsfds 数组中的元素个数。
  • timeout: 超时时间(以毫秒为单位)。如果 timeout 为负数,poll 将永远阻塞直到有文件描述符准备就绪。如果 timeout 为 0,poll 会立即返回,即不堵塞。

events 和 revents 字段的事件掩码

常用的事件掩码包括:

  • POLLIN: 有数据可读。
  • POLLRDNORM: 普通数据可读。
  • POLLRDBAND: 优先数据可读。
  • POLLPRI: 有紧急数据可读。
  • POLLOUT: 可以写数据。
  • POLLWRNORM: 普通数据可写。
  • POLLWRBAND: 优先数据可写。
  • POLLERR: 发生错误。
  • POLLHUP: 挂起。
  • POLLNVAL: 描述符非法。

返回值

  • 返回值为正数:表示准备就绪的文件描述符数量。
  • 返回值为 0:表示在指定的超时时间内没有文件描述符准备就绪。
  • 返回值为 -1:表示出现错误,同时设置 errno 以指示错误类型。

在使用 poll 函数时,pollfd 结构体中的 eventsrevents 字段分别表示要监控的事件和实际发生的事件。它们的具体区别如下:

events 字段

events 字段用于指定你希望 poll 函数监控的事件类型。你可以通过设置 events 字段来告诉 poll 函数你对哪些事件感兴趣。常用的事件掩码包括:

  • POLLIN: 有数据可读。
  • POLLRDNORM: 普通数据可读。
  • POLLRDBAND: 优先数据可读。
  • POLLPRI: 有紧急数据可读。
  • POLLOUT: 可以写数据。
  • POLLWRNORM: 普通数据可写。
  • POLLWRBAND: 优先数据可写。
  • POLLERR: 发生错误。
  • POLLHUP: 挂起。
  • POLLNVAL: 描述符非法。

例如,如果你希望监控一个文件描述符是否有数据可读,你可以设置 events 字段为 POLLIN

revents 字段

revents 字段用于指示 poll 函数返回时实际发生的事件类型。poll 函数会在返回时将 revents 字段设置为实际发生的事件的掩码。你可以通过检查 revents 字段来确定哪些事件在调用 poll 函数期间发生了。

例如,如果 poll 函数返回时 revents 字段包含 POLLIN,这意味着在调用 poll 函数期间,有数据可读。


多客户端聊天服务器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define PORT 12345
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024void error(const char *msg) {perror(msg);exit(1);
}int main() {int server_fd, new_socket, client_socket[MAX_CLIENTS], max_sd, sd, activity, valread;int opt = 1;struct sockaddr_in address;fd_set readfds;char buffer[BUFFER_SIZE];// 初始化所有客户端套接字为0(未使用)for (int i = 0; i < MAX_CLIENTS; i++) {client_socket[i] = 0;}// 创建服务器套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {error("socket failed");}// 设置服务器套接字选项if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)) < 0) {error("setsockopt");}// 配置服务器地址address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 绑定套接字到端口if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {error("bind failed");}// 监听连接if (listen(server_fd, 3) < 0) {error("listen");}printf("Listening on port %d \n", PORT);while (1) {// 清空读文件描述符集合FD_ZERO(&readfds);// 将服务器套接字加入集合FD_SET(server_fd, &readfds);max_sd = server_fd;// 添加客户端套接字到集合for (int i = 0; i < MAX_CLIENTS; i++) {sd = client_socket[i];if (sd > 0) {FD_SET(sd, &readfds);}if (sd > max_sd) {max_sd = sd;}}// 等待活动activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);// 检查是否有新的连接if (FD_ISSET(server_fd, &readfds)) {int addrlen = sizeof(address);if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {error("accept");}printf("New connection, socket fd is %d, ip is: %s, port: %d\n", new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));// 将新套接字加入客户端数组for (int i = 0; i < MAX_CLIENTS; i++) {if (client_socket[i] == 0) {client_socket[i] = new_socket;printf("Adding to list of sockets as %d\n", i);break;}}}// 检查客户端套接字是否有数据for (int i = 0; i < MAX_CLIENTS; i++) {sd = client_socket[i];if (FD_ISSET(sd, &readfds)) {// 检查是否是关闭连接if ((valread = read(sd, buffer, BUFFER_SIZE)) == 0) {printf("Host disconnected, ip %s, port %d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));// 关闭套接字并从集合中移除close(sd);client_socket[i] = 0;} else {// 处理客户端发送的数据buffer[valread] = '\0';printf("Received message: %s\n", buffer);// 广播给其他客户端for (int j = 0; j < MAX_CLIENTS; j++) {if (client_socket[j] != 0 && client_socket[j] != sd) {send(client_socket[j], buffer, strlen(buffer), 0);}}}}}}return 0;
}

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

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

相关文章

利用聚合API平台的API接口,利用HTTP协议向服务器发送请求,并接受服务器的响应,要求利用cJSON库对服务器的响应数据进行解析,并输出到终端

目录题目分析代码结果 题目利用聚合API平台的API接口,利用HTTP协议向服务器发送请求,并接受服务器的响应,要求利用cJSON库对服务器的响应数据进行解析,并输出到终端 分析1.需从源代码网站GitHub或SourceForge代码网站下载cJSON库及阅读下载的README相关手册如何使用cJSON库…

[DP] [倍增优化] Luogu P1081 [NOIP2012 提高组] 开车旅行

[NOIP2012 提高组] 开车旅行 题目描述 小 \(\text{A}\) 和小 \(\text{B}\) 决定利用假期外出旅行,他们将想去的城市从 $1 $ 到 \(n\) 编号,且编号较小的城市在编号较大的城市的西边,已知各个城市的海拔高度互不相同,记城市 \(i\) 的海拔高度为\(h_i\),城市 \(i\) 和城市 \…

【esp32 学习笔记】让SD卡发光发热~

原理图:图 SD卡部分图 MCU中与SD卡相关的接口 连接关系如下:[ESP32 IO26 – CS MICROSD] [ESP32 IO23 – MOSI(DI) MICROSD] [ESP32 IO19 – MISO(DO) MICROSD] [ESP32 IO18 – SCK MICROSD] [ESP32 GND – GND MICROSD] [3.3V – VCC MICROSD] 软件: 我们将使用SD卡库…

网络编程练习题---利用cJSON库对服务器返回的JSON格式数据完成解析

利用HTTP协议向聚合数据API发送请求,并利用cJSON库对服务器返回的JSON格式数据完成解析目录题目注意事项实现代码结果展示相关接口指引 题目利用某些平台(聚合API、百度AI、科大讯飞API)的API接口,利用HTTP协议向服务器发送请求,并接受服务器的响应,要求利用cJSON库对服务…

嵌入式Linux中的LED驱动控制(续)

前面的实例实现了在野火STM32MP157开发板上对三个LED灯的控制,这里来讨论一下该驱动程序的具体实现方式。由于实例使用的是STM32MP157这款芯片,所以先来看一下与该芯片端口操作相关的寄存器。 先看端口模式寄存器MODER,该类型的寄存器在STM32MP157中有11个,即x的值从A到K。…

通讯协议转换Modbus转Profinet网关

Modbus转Profinet网关是工业通信转换设备,能够实现Modbus协议与Profinet协议之间的有效转换和稳定传输。通过该网关,工业设备之间可以实现数据交换和通信,提高生产效率和智能化程度。支持Modbus RTU主从站。此外,Modbus转 Profinet网关自带网络和串口,支持485/232接口Modb…

python栈帧沙箱逃逸

python栈帧沙箱逃逸 一、生成器 生成器(Generator)是 Python 中一种特殊的迭代器,它可以通过简单的函数和表达式来创建。生成器的主要特点是能够逐个产生值,并且在每次生成值后保留当前的状态,以便下次调用时可以继续生成值。这使得生成器非常适合处理大型数据集或需要延迟…

spring-1-IOC、创建bean的方式、创建bean的过程

1.背景 IOC(Inversion of Control,控制反转) 控制反转是一种设计原则,它将对象的创建和管理责任从应用代码中移交给容器。 在Spring中,IOC容器负责管理应用中的所有对象,包括它们的生命周期和相互之间的依赖关系。 IOC的主要目的是为了减少代码之间的耦合,使代码更加模块…