【网络】【Linux】多路转接技术

【网络】【Linux】多路转接技术

多路转接技术

文章目录

在之前学习五种IO模型时,我们认识到了IO的本质是等+拷贝,而多路转接技术可以让等的过程重叠,即同时等待多个文件描述符的就绪状态,所以今天我们就来学习下如何等待多个文件描述就绪。

本篇文章会介绍三种实现多路转接的系统调用接口,实际最常用的是epoll,其实一些老的机器上面只兼容select,而poll并不常用。

1.select

select是系统提供的一个多路转接接口。

  • select系统调用可以让我们的程序同时监视多个文件描述符的上的事件是否就绪。
  • select的核心工作就是等,当监视的多个文件描述符中有一个或多个事件就绪时,select才会成功返回并将对应文件描述符的就绪事件告知调用者。

1.1select系统调用及参数介绍

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 

参数说明:

  • nfds:需要监视的文件描述符中,最大的文件描述符值+1(select底层使用for循环遍历实现,该值是为了界定遍历范围)。
  • readfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪。
  • writefds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪。
  • exceptfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已经就绪。
  • timeout:输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间

参数timeout的取值:

  • NULL/nullptr:select调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
  • 0:selec调用后t进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。
  • 特定的时间值:select调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后select进行超时返回。

返回值说明:

  • 如果函数调用成功,则返回有事件就绪的文件描述符个数。
  • 如果timeout时间耗尽,则返回0。
  • 如果函数调用失败,则返回-1,同时错误码会被设置。

select调用失败时,错误码可能被设置为:

  • EBADF:文件描述符为无效的或该文件已关闭。
  • EINTR:此调用被信号所中断。
  • EINVAL:参数nfds为负值。
  • ENOMEM:核心内存不足。

(1)fd_set类型

fd_set类型可以理解为一个位图,每个比特位位置代表是哪个文件描述符,值代表是否就绪。

调用select函数之前就需要用fd_set结构定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集当中,这个添加的过程本质就是在进行位操作,但是这个位操作不需要用户自己进行,系统提供了一组专门的接口,用于对fd_set类型的位图进行各种操作。

void FD_CLR(int fd, fd_set *set); //用来清除描述词组set中相关fd的位 int FD_ISSET(int fd, fd_set *set); //用来测试描述词组set中相关fd的位是否为真 void FD_SET(int fd, fd_set *set); //用来设置描述词组set中相关fd的位 void FD_ZERO(fd_set *set); //用来清除描述词组set的全部位 

(2)timeval结构

传入select函数的最后一个参数timeout,就是一个指向timeval结构的指针,timeval结构用于描述一段时间长度,该结构当中包含两个成员,其中tv_sec表示的是秒,tv_usec表示的是微秒。

调用时由用户设置select的等待时间,返回时表示timeout的剩余时间

(3)socket就绪条件

读就绪
  • socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读取该文件描述符,并且返回值大于0。
  • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0。
  • 监听的socket上有新的连接请求。
  • socket上有未处理的错误。
写就绪
  • socket内核中,发送缓冲区中的可用字节数,大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。
  • socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号。=
  • socket使用非阻塞connect连接成功或失败之后。
  • socket上有未读取的错误。

1.2select基本工作流程

利用select多路转接实现一个简单的Echo服务器,该服务器要做的就是读取客户端发来的数据并进行打印:

  1. 先初始化服务器,完成套接字的创建、绑定和监听。
  2. 定义一个fd_array辅助数组用于保存监听套接字和已经与客户端建立连接的套接字,刚开始时就将监听套接字添加到fd_array数组当中。
  3. 然后服务器开始循环调用select函数,检测读事件是否就绪,如果就绪则执行对应的操作。
  4. 每次调用select函数之前,都需要定义一个读文件描述符集readfds,并将fd_array辅助数组当中保存的文件描述符依次设置进readfds当中表示让select帮我们监视这些文件描述符的读事件是否就绪。
  5. 当select检测到数据就绪时会将读事件就绪的文件描述符设置进readfds当中,此时我们就能通过提取readfds中的信息得知哪些文件描述符已经就绪,并对这些文件描述符进行对应的操作。
    • 如果读事件就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接,并将该连接对应的套接字添加到fd_array数组当中。
    • 如果读事件就绪的是与客户端建立连接的套接字,则调用read函数读取客户端发来的数据并进行打印输出。
  6. 当然,服务器与客户端建立连接的套接字读事件就绪,也可能是因为客户端将连接关闭了,此时服务器应该调用close关闭该套接字,并将该套接字从fd_array辅助数组当中清除,因为下一次不需要再监视该文件描述符的读事件了。

为什么要有辅助数组?

  • 在服务器程序中,随着客户端连接的建立和断开,需要监听的文件描述符集合会动态变化。select调用后,只有发生事件的文件描述符会被保留在集合中,未发生事件的文件描述符会被清除。
  • 因此,每次调用select之前,都需要重新构建文件描述符集合,确保所有需要监听的文件描述符都被包括在内。辅助数组可以用来存储当前所有需要监听的文件描述符,方便在每次调用select之前重新构建fd_set

说明

  • 服务器刚开始运行时,fd_array数组当中只有监听套接字,因此select第一次调用时只需要监视监听套接字的读事件是否就绪,但每次调用accept获取到新连接后,都会将新连接对应的套接字添加到fd_array当中,因此后续select调用时就需要监视监听套接字和若干连接套接字的读事件是否就绪。
  • 由于调用select时还需要传入被监视的文件描述符中最大文件描述符值+1,因此每次在遍历fd_array对readfds进行重新设置时,还需要记录最大文件描述符值。

这其中还有很多细节,下面我们就来实现这样一个select服务器(这里我们只对读取实现多路转接)。

1.3select技术实现echo服务器

(1)构造服务器

对于一个Tcp服务器来说,我们首先需要创建一个listen套接字,然后在该listen套接字上等待获取新连接(普通套接字),而这个等待新连接到来的行为等价于对方给我发送数据,所以我们将获取新连接的行为看作读事件,所以在构造时,创建完listen套接字后,我们还需要将listen套接字仿佛辅助数组,未来由select统一进行等待。

SelectServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>()) { InetAddr addr("0", _port); _listensock->BuildListenSocket(addr); for (int i = 0; i < N; i++) { _fd_array[i] = defaultfd; } // listensocket 等待新连接到来,等价于对方给我发送数据!我们作为读事件统一处理 // 新连接到来 等价于 读事件就绪! // 首先要将listensock添加到select中! _fd_array[0] = _listensock->SockFd(); // 首先将listen套接字放入辅助数组 } 

(2)服务器核心逻辑Loop

服务器首先必须要有一个Loop()方法,是服务器执行的主逻辑,服务器就是一个死循环。

我们需要利用select监视读事件,所以每次循环都需要首先要初始化出来一个fd_set结构,并利用FD_ZERO将该文件描述符集内容清空,然后将辅助数组中保存的就绪的文件描述符通过FD_SET函数赋值给文件描述符集,注意更新最大文件描述符的值。(这里我们只考虑读事件,所以写事件和异常事件我们不考虑)。

之后我们就可以填充timeval结构:

  • NULL/nullptr:select调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
  • 0:selec调用后t进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。
  • 特定的时间值:select调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后select进行超时返回。

然后根据select的返回值打印日志,当select返回值>0时(返回值是有事件就绪的文件描述符个数),证明监视的套接字中有读事件发生,此时对读事件进行处理,这里我们封装一个HandlerEvent函数,表示对读事件处理。

void Loop() { while (true) { fd_set rfds; FD_ZERO(&rfds); // 将文件描述符集清空 int max_fd = defaultfd; for (int i = 0; i < N; i++) { if (_fd_array[i] == defaultfd) { continue; } FD_SET(_fd_array[i], &rfds); // 将所有合法的fd添加到rfds中 if (max_fd < _fd_array[i]) { max_fd = _fd_array[i]; // 更新出最大的fd的值 } } struct timeval timeout = {0, 0}; // select 同时等待的fd,是有上限的。因为fd_set是具体的数据类型,有自己的大小! // rfds是一个输入输出型参数,每次调用,都要对rfds进行重新设定! int n = select(max_fd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr); switch (n) { case 0: LOG(INFO, "timeout, %d.%d\n", timeout.tv_sec, timeout.tv_usec); break; case -1: LOG(ERROR, "select error...\n"); break; default: LOG(DEBUG, "Event Happen. n : %d\n", n); HandlerEvent(rfds); break; } } } 

(3)对读事件进行处理HandlerEvent

调用该函数时,证明此时rfds文件描述符集已经被设定(输入输出型参数),文件描述符集中就是哪些文件描述符发生读事件,需要进行处理,所以我们只需要遍历辅助数组中保存的文件描述符,检测他们在rfds中是否被设定,如果被设定证明在该文件描述符上有事件发生需要进行处理,然后只需要分成两种情况,一种是listen套接字发生读事件,此时需要获取连接,另一种就是普通套接字,此时进行IO处理即可。

void HandlerEvent(fd_set &rfds) { for (int i = 0; i < N; i++) { if (_fd_array[i] == defaultfd) continue; if (FD_ISSET(_

Read more

Python零基础入门教程:从环境搭建到实战项目(超详细图文详解)

Python零基础入门教程:从环境搭建到实战项目(超详细图文详解)

文章目录 * Python基础入门教程:从零开始学编程(超详细版) * 一、前言 * 二、环境搭建(详细步骤) * 1. 安装Python * Windows系统: * macOS系统: * Linux系统(以Ubuntu为例): * 2. 开发工具推荐 * PyCharm(专业版/社区版): * VS Code(轻量级): * Jupyter Notebook(交互式开发): * 三、基础语法详解 * 1. 第一个Python程序 * 2. 注释规范 * 3. 变量与数据类型(详细说明) * 变量命名规则: * 核心数据类型: * 类型转换: * 四、常用数据类型详解 * 1. 列表(List) * 基本操作: * 切片操作: * 2. 元组(Tuple) * 特点: * 3.

By Ne0inhk
【Python】基础语法入门(一)

【Python】基础语法入门(一)

前言 Python作为一门入门门槛低、生态丰富的编程语言,Python早已成为编程初学者、数据分析从业者、后端开发者的首选工具之一。而掌握Python的第一步,就是吃透最核心的基础语法,常量与表达式、变量与类型、注释、输入输出及运算符。今天,我们就结合实例,手把手带你入门这些必备知识点,助你快速搭建Python语法框架。 一、常量与表达式 刚接触 Python 时,我们可以先把它当作一个功能强大的计算器 ,通过简单的表达式,以完成各类算术运算,比如简单的加减乘除,甚至复杂的乘方运算,都能直接通过“表达式”实现。 核心知识点: 1. 表达式与常量:形如1 + 2 * 3的算式称为“表达式”,运算结果为“表达式的返回值”;1、2、3这类固定值称为“字面值常量”,+、-、*、/则是“运算符”。 2. 运算规则:遵循“先乘除后加减”的数学逻辑,

By Ne0inhk
OpenClaw 都在排队养,你还在云端白嫖?手把手教你用 Python 搭建本地 AI 智能体(小白也能养自己的小龙虾)

OpenClaw 都在排队养,你还在云端白嫖?手把手教你用 Python 搭建本地 AI 智能体(小白也能养自己的小龙虾)

🦞 长文警告! 📜 文章目录(点击跳转,这波操作稳如老狗) 1. 前言:别再当云端 AI 的韭菜了,把“小龙虾”养在自己家 2. 第一步:给电脑装个“胃”——下载安装 Python(含官网地址) 3. 第二步:请个本地“大脑”——Ollama + Qwen 模型(白嫖党狂喜) 4. 第三步:搭个“龙虾笼子”——安装 OpenClaw(附项目地址) 5. 第四步:用 Python 写个“传话筒”,让你的小龙虾听你指挥 6. 第五步:第一次对话——你的本地贾维斯上线 7. 总结:白嫖虽好,但别让龙虾把你的电脑“钳”

By Ne0inhk
【C++】类与对象初级应用篇:打造自定义日期类与日期计算器(2w5k字长文附源码)

【C++】类与对象初级应用篇:打造自定义日期类与日期计算器(2w5k字长文附源码)

文章目录 * 一、日期类的实现 * 1. 日期类的默认成员函数的分析与实现 * 构造函数 * 其它默认成员函数 * 2. 各种逻辑比较运算符重载 * 3. 日期加与减天数 * 日期加天数系列 * 日期减天数系列 * 日期加减天数的最后修定 * ++和- -系列 * 4. 日期减日期 * 方法一 * 方法二 * 5. 流插入与流提取重载 * 流插入重载 * 流提取重载(含修正默认构造) * 6. 源码 * 二、基于日期类实现日期计算器 一、日期类的实现 在前面的内容中,我们讲解了六大默认成员函数,基本上对类和对象有了简单的认识,今天这篇文章我们就来实现一下之前一直拿来举例用的日期类,顺便基于日期类实现一下日期计算机,做点实用的东西让大家感觉到自己的进步 在开始正式学习之前,我们先在这里做一下强调,就是我们在实现日期类的时候,采用声明和定义分离的方式来写,这样使得我们的代码的可读性更高,声明写在头文件中,定义写在.cpp文件中,如下

By Ne0inhk