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

【Linux】多路转接select

目录

一、初识select

1.1 select函数原型

1.2 常见的程序片段

二、理解select执行过程

2.1 socket就绪条件

2.2 select特点

2.3 select缺点

三、select使用示例:检测标准输入标准输出

四、select实现字典服务器


一、初识select

系统提供select函数来实现多路转接输入/输出模型。

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。

1.1 select函数原型

select函数原型如下:

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

参数解释

  • 参数nfds是需要监视的最大的文件描述符值+1。
  • rdset,wrset,exset分别对应于需要监视的可读文件描述符的集合,可写文件描述符的集合以及异常文件描述符的集合。
  • 参数timeout为结构timeval,用来设置select的等待时间。

参数timeout取值

  • NULL:则表示select没有timeout,select将一直被阻塞,知道某个文件描述符上发生了事件。
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段中没有事件发生,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的全部位

关于timevla结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值是0。

函数返回值

  • 执行成功则返回文件描述符状态已改变的个数。
  • 如果返回0表示在文件描述符状态改变之前已超过timeout的时间,没有返回。
  • 当有错误发生时,则返回-1,错误原因则存于errno,此时参数readfds、writefds、exceptfds和timeout的值变成不可预测。

错误值可能为

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

1.2 常见的程序片段

fd_set readset;
FD_SET(fd, &readset);
select(fd+1, &readset, NULL, NULL, NULL);
if(FD_ISSET(fd, readset)) {// ...
}

二、理解select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则一字节长的fd_set最大可以对应8个fd。

  1. 执行fd_set set; FD_ZERO(&set); 则set用位表示是0000 0000。
  2. 若fd=5,执行FD_SET(fd, &set); 后set变为0001 0000(第五位变为1)。
  3. 若再加入fd=2,fd=1,则set变为0001 0011。
  4. 执行select(6, &set, 0, 0, 0)阻塞等待。
  5. 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000 0011。注意:没有事件发生的fd=5被清空。

2.1 socket就绪条件

读就绪

  • socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT。此时可以无阻塞的读该文件描述符,并且返回值大于0。
  • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0。
  • 监听的socket上有新的连接请求。
  • socket上有未处理的错误。

写就绪

  • socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。
  • socket的写操作被关闭(close过着shutdown)。对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
  • socket使用非阻塞connect连接成功或失败之后。
  • socket上有未读取的错误。

异常就绪

  • socket上收到带外数据。关于带外数据和TCP紧急模式相关(回忆TCP协议头中,有一个紧急指针的字段),感兴趣可以去了解一下。

2.2 select特点

  • 可监控的文件描述符个数取决于sizeof(fd_set)的值。例如我这个服务器上sizeof(fd_set)=128,每个bit表示一个文件描述符,则我服务器上支持的最大文件描述符是128*8=1024。
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>int main()
{std::cout << sizeof(fd_set) << std::endl;return 0;
}

  • 将fd加入select监控集的同时,还要在使用一个数据结构array保存放到select监控集中的fd。
  1. 用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断。
  2. select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array同时取得fd最大值maxfd,用于select的第一个参数。

备注:

fd_set的大小可以调整,可能涉及到重新编译内核。感兴趣可以了解一下。

2.3 select缺点

  • 每次调用select,都要手动设置fd集合,从接口使用角度来说也非常不方便。
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
  • select支持的文件描述符数量太小了。

三、select使用示例:检测标准输入标准输出

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>int main()
{fd_set read_fds;FD_ZERO(&read_fds);FD_SET(0, &read_fds);while(1) {printf("> ");fflush(stdout);int ret = select(1, &read_fds, nullptr, nullptr, nullptr);if(ret < 0) {perror("select");continue;}if(FD_ISSET(0, &read_fds)) {char buf[1024];ssize_t n = read(0, buf, sizeof(buf) - 1);if(n > 0) {buf[n] = 0;std::cout << buf;}}FD_ZERO(&read_fds);FD_SET(0, &read_fds);}return 0;
}

说明:

当只检测文件描述符0(标准输入)时,因为输入条件只在你有输入信息的时候才成立,所以如果一直不输入,就会产生超时信息。

四、select实现字典服务器

// SelectServer.hpp#pragma once#include <iostream>
#include <string>
#include <memory>
#include <sys/select.h>
#include "Log.hpp"
#include "Socket.hpp"using namespace LogModule;
using namespace SocketModule;#define NUM sizeof(fd_set) * 8const static int gdefaultfd = -1;class SelectServer
{
public:SelectServer(uint16_t port):_port(port),_listen_socket(new TcpSocket),_running(false){}void Init() {_listen_socket->BuildListenMethod(_port);for(int i = 0; i < NUM; i++)_fd_array[i] = gdefaultfd;_fd_array[0] = _listen_socket->GetSockfd();}void Loop() {fd_set fds;_running = true;while(_running) {FD_ZERO(&fds);struct timeval timeout = {10, 0};int maxfd = gdefaultfd;for(int i = 0; i < NUM; i++) {if(_fd_array[i] != gdefaultfd) {FD_SET(_fd_array[i], &fds);if(maxfd < _fd_array[i]) maxfd = _fd_array[i];}}int n = select(maxfd + 1, &fds, nullptr, nullptr, &timeout);switch(n) {case 0:std::cout << "time out..." << std::endl;break;case -1:perror("select");break;default:// 有事件就绪了std::cout << "有事件就绪了,timeout:" << timeout.tv_sec << ":" << timeout.tv_usec << std::endl;Dispatcher(fds); // 将已经就绪的sockfd,派发给指定模块break;}}_running = false;}void Dispatcher(fd_set &fds) {for(int i = 0; i < NUM; i++) {if(_fd_array[i] == gdefaultfd) continue;if(FD_ISSET(_fd_array[i], &fds)) {if(_fd_array[i] == _listen_socket->GetSockfd()) {Accepter();}else Recver(i);}}}void Accepter() {InetAddr client;auto cli_socket = _listen_socket->Accept(&client);if(!cli_socket) return;LOG(LogLevel::DEBUG) << "get a new client, info is: " << client.Addr();// 将得到客户端文件描述符插入到fd集合中int pos = -1;for(int i = 0; i < NUM; i++)if(_fd_array[i] == gdefaultfd) {pos = i;break;}if(pos == -1) {LOG(LogLevel::WARNING) << "服务器已经满了...";close(cli_socket->GetSockfd());}else _fd_array[pos] = cli_socket->GetSockfd();}void Recver(int who) {char buf[1024];ssize_t n = recv(_fd_array[who], buf, sizeof(buf) - 1, 0);if(n == 0) {LOG(LogLevel::DEBUG) << "客户端退出,sockfd: " << _fd_array[who];close(_fd_array[who]);_fd_array[who] = gdefaultfd;}else if(n > 0) {buf[n] = 0;std::cout << "client# " << buf << std::endl;std::string message = "echo# ";message += buf;send(_fd_array[who], message.c_str(), message.size(), 0);}else {LOG(LogLevel::ERROR) << "客户端读取出错,sockfd: " << _fd_array[who];close(_fd_array[who]);_fd_array[who] = gdefaultfd;}}
private:uint16_t _port;Socket* _listen_socket;bool _running;int _fd_array[NUM];
};
// SelectServer.cc#include "SelectServer.hpp"int main(int argc, char* argv[])
{if(argc != 2) {std::cout << "Usage: " << argv[0] << " server_port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);SelectServer ss(port);ss.Init();ss.Loop();return 0;
}

客户端代码使用的是我们之前学习TCP套接字写的TcpClient.cc的代码,这里就不重复了。

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

相关文章:

  • Python基础语法4
  • 网站后台管理要求软文怎么优化网站
  • RAG的检索与排序增强实现原理
  • 【计算机网络】物理层设备核心考点精讲:物理层设备(中继器/集线器)全解析
  • C++虚函数机制与重写规范:从原理到实践
  • vben admin 实现实时监听表格复选框
  • 建站合作设计教育网站
  • 基于ArcGIS的动物迁移生态廊道规划案例 | 人与万物,共生共荣
  • 网站开发小程序做网站优化多少钱
  • 扩散模型入门:原理、训练与生成全解析
  • 使用 GitLab CI/CD 为 Linux 构建 RPM 包(二)
  • 图的邻接表实现及遍历
  • 使用仓颉语言实现 nanoid:一个安全的唯一 ID 生成器
  • 语义模型 - 从 Transformer 到 Qwen
  • 前端零基础速成前端开发路线
  • 《系统规划与管理师教程(第2版)》方法篇 第10章 云原生系统规划 知识点总结
  • 有没有让人做问卷的网站中国深圳航空公司官方网站
  • springcloud二-Seata3- Seata各事务模式
  • MySQL 全链路性能调优:从 “凌晨三点被叫醒“ 到 “0.1 秒响应“ 的实战心法(超能优化版)
  • linux命令-用户管理-7
  • 【JavaScript】Pointer Events 与移动端交互
  • 客户评价 网站织梦cms侵权
  • 文件上传下载
  • 深入GoChannel:并发编程的底层奥秘
  • JS面试基础(一) 垃圾回收,变量与运算符
  • 2025年渗透测试面试题总结-225(题目+回答)
  • 重庆电商平台网站建设合肥推广优化公司
  • Linux命令行基础:常用命令快速上手(附代码示例)
  • 在Ubuntu Desktop操作系统下,rustdesk客户端如何设置成开机自动启动?
  • 建设静态网站怎么制作网页链接在微信上发