【Linux 34】多路转接 - select

server/2025/1/15 22:00:58/

文章目录

  • 🌈 一、select 初步认识
  • 🌈 二、select 函数原型
  • 🌈 三、select 执行过程
  • 🌈 四、socket 就绪条件
    • ⭐ 读就绪
    • ⭐ 写就绪
  • 🌈 五、select 的优缺点
    • ⭐ select 的优点
    • ⭐ select 的缺点
  • 🌈 六、select 使用示例
    • ⭐ Socket 类
    • ⭐ select_server 类

🌈 一、select 初步认识

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

举个例子

在这里插入图片描述

  • “我”(调用者)找来了一个快递通知员(select),它会帮我监视我所指定的快递员(文件描述符)。每当有快递到达(事件就绪)时,select 就会通知 “我” 快递到来(事件已就绪)。
  • 但是,select 只会告诉 “我” 快递到来,但并不会告诉 “我” 这个快递是谁送来的(不知道就绪事件发生在哪个文件描述符上)。此时就需要 “我” 对每个快递员都询问一遍。
  • select 只会通知调用者事件就绪了(快递到了),快递此时还在快递员手上拿着,接收快递这个动作依然要由 “我” 来完成。
  • 当事件就绪时,如果 “我” 一直不做处理,select 就会一直进行通知,直到该事件被处理了为止。

🌈 二、select 函数原型

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

1. 参数说明

  • nfds:需要监视的所有的文件描述符中,最大的那个文件描述符的值 + 1。
  • readfds:输入输出型参数。调用时,用户告知内核需要监视哪些文件描述符的读事件是否就绪。返回时,内核告知用户哪些文件描述符的读事件就绪。
  • writefds:输入输出型参数。调用时,用户告知内核需要监视哪些文件描述符的写事件是否就绪。返回时,内核告知用户哪些文件描述符的写事件就绪。
  • exceptfds:输入输出型参数。调用时,用户告知内核需要监视哪些文件描述符的异常事件是否就绪。返回时,内核告知用户哪些文件描述符的异常事件就绪。
  • timeout:输入输出型参数。调用时,由用户设置 select 的等待时间。返回时,表示 timeout 的剩余时间。

2. 参数 timeout 的取值

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

3. 返回值说明

  • 函数调用成功:返回有事件就绪的文件描述符的个数。
  • timeout 时间耗尽:返回 0。
  • 函数调用失败:返回 -1,同时错误码会被设置。错误码可能的取值如下:
    • EBADF:文件描述符无效 / 该文件已经关闭
    • EINTR:此调用被信号所中断
    • EINVAL:参数 nfds 为负数
    • ENOMEM:核心内存不足

4. 关于 fd_set 文件描述符集的结构

  • select 函数的返回值只会告知调用者,被 select 所监视的文件描述符上是否有事件就绪,并不会告知是哪些文件描述符上的事件就绪。当事件就绪时,调用者要从 fd_set 这个输入输出参数中获取事件就绪的文件描述符。
  • fd_setsigset_t 的结构类似,本质上也是一个位图。它使用位图中对应的位来表示要监视的文件描述符。

image-20250112181507053

image-20250112181547745

  • 在调用 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 的全部位

5. 关于 timeval 结构

  • 传给 select 函数的最后一个 timeout 参数,它是一个用来指向 timeval 结构的指针。
  • timeval 结构是用来描述一段时间长度,该结构体中包含着 tv_sec(秒)和 tv_usec(微秒)两个成员变量。

image-20250112182309966

🌈 三、select 执行过程

  • 想要理解 select 模型,关键就在于理解 fd_set
  • 取 fd_set 的长度为 1 字节,fd_set 中的每一个 bit 都可以对应一个文件描述符。则 1 字节厂的 fd_set 最大可对应 8 个文件描述符。
  1. 执行 fd_set set; FD_ZERO(&set);则 set 用位表示是 0000 0000
  2. 若 fd = 5,执行 FD_SET(fd,&set);后 set 变为 0001 0000 (第 5 位置为 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 被清空。

🌈 四、socket 就绪条件

⭐ 读就绪

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

⭐ 写就绪

  • 在 socket 内核中,发送缓冲区的可用字节数 >= 低水位标记 SO_SNDLWOAT 时,可以无阻塞的进行写入,且返回值 > 0
  • 如果 socket 已经被关闭(close / shutdown),对这个被关闭的 socket 进行写操作,会触发 SINGPIPE 信号。
  • socket 使用非阻塞 connect 连接成功或失败后。
  • socket 上有未被读取的数据。

🌈 五、select 的优缺点

⭐ select 的优点

  • 可同时等待多个文件描述符上的事件就绪,且只负责等待。实际的 IO 操作由 accept、read、write 等接口来实现,这些接口在进行 IO 操作时不会被阻塞。
  • 由于 select 可以同时等待多个文件描述符,因此可以将这个 等 的时间进行重叠,提高 IO 效率。

⭐ select 的缺点

  • 每次调用 select 时,都需要手动设置 fd_set 文件描述符集,不方便使用。
  • 每次调用 select 时,都需要将 fd_set 从用户态拷贝到内核态,在 fd 很多时开销大。
  • 每次调用 select 时,都需要在内核遍历传递进来的所有 fd,在 fd 很多事开销大。
  • select 可监控的文件描述符数量太少(只有 sizeof(fd_set) * 8 == 1024 个)。

🌈 六、select 使用示例

  • 利用 select 来实现一个服务器。

⭐ Socket 类

#pragma once#include <string>
#include <memory>
#include <cstring>
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <strings.h>
#include <functional>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>#include "log.h"
#include "inet_addr.h"using std::bind;
using std::cerr;
using std::cin;
using std::cout;
using std::endl;
using std::function;
using std::make_shared;
using std::make_unique;
using std::shared_ptr;
using std::stoi;
using std::string;
using std::to_string;
using std::unique_ptr;// 封装: 模板方法模式
namespace socket_ns
{class Socket;const static int gbacklog = 8;using socket_sptr = shared_ptr<Socket>;enum{SOCKET_ERROR = 1,USAGE_ERROR = 2,BIND_ERROR = 3,LISTEN_ERROR = 4,CONNECT_ERROR = 5};class Socket{public:// 声明纯虚函数virtual void create_socket_or_die() = 0;              // 创建套接字,如果不成功则直接结束virtual void bind_socket_or_die(uint16_t port) = 0; // 绑定套接字,如果不成功则直接结束virtual void listen_socket_or_die() = 0;              // 监听套接字,如果不成功则直接结束virtual int accepter(Inet_addr *addr) = 0;    		// 获取连接virtual bool connetcor(Inet_addr &addr) = 0;          // 连接服务器virtual int sockfd() = 0;                             // 用户获取套接字virtual int Recv(string *out) = 0;                    // 读取信息放到 out 中,并返回读取到的字符串的长度virtual int Send(const string &in) = 0;               // 发送添加处理好并添加好报头的数据virtual void reuse_address() = 0;                     // 地址复用public:// 创建监听套接字void build_listen_socket(uint16_t port){create_socket_or_die();   // 创建套接字reuse_address();          // 对该套接字进行地址复用bind_socket_or_die(port); // 绑定套接字listen_socket_or_die();   // 监听套接字}// 创建客户端的套接字bool build_client_socket(Inet_addr &addr){create_socket_or_die(); // 创建套接字return connetcor(addr); // 向服务器发起连接}};class tcp_socket : public Socket{private:int _sockfd;public:tcp_socket(int fd = -1): _sockfd(fd){}/*纯虚函数不能实例化出对象,需要重写纯虚函数*/// 创建套接字,如果不成功则直接结束void create_socket_or_die() override{// 创建流式套接字_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(FATAL, "create socket error");exit(SOCKET_ERROR);}LOG(DEBUG, "create socket success, sockfd is %d", _sockfd);}// 绑定套接字,如果不成功则直接结束void bind_socket_or_die(uint16_t port) override{// 填充 sockaddr_in 结构struct sockaddr_in local;            // 用于和本地套接字关联bzero(&local, sizeof(local));        // 清空 local 所占用的内存空间local.sin_family = AF_INET;          // 地址协议家族: 本地通信 or 网络通信local.sin_port = htons(port); 		// 将端口号从主机序列转成网络序列 (大端)local.sin_addr.s_addr = INADDR_ANY;  // 绑定服务器上任意 IP// 绑定 socket 文件信息和网络信息int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "socket bind error, %s %d", strerror(errno), errno);exit(BIND_ERROR);}LOG(DEBUG, "socket bind success, sockfd is %d", _sockfd);}// 监听套接字,如果不成功则直接结束void listen_socket_or_die() override{// 4.监听//  TCP 是面向连接的,通信之前必须先建立连接,//  服务器是要被连接的,服务器要一直等待客户的连接int n = ::listen(_sockfd, gbacklog); // 对对应的套接字进行监听,以等待用户向服务器发起连接if (n < 0)                           // 监听失败{LOG(FATAL, "listen error");exit(LISTEN_ERROR);}LOG(DEBUG, "listen success, sockfd is %d", _sockfd);}// 服务器获取连接int accepter(Inet_addr *addr) override{struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);// 获取新连接失败if (sockfd < 0){LOG(WARNING, "accept error");return -1;}*addr = Inet_addr(peer);return sockfd;}// 客户端连接服务器bool connetcor(Inet_addr &addr) override{// 获取服务器的套接字信息struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(addr.port());server.sin_addr.s_addr = inet_addr(addr.ip().c_str());// 3.客户端通过套接字向服务器发起连接int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));// 发起连接失败if (n < 0){cerr << "connect error" << endl;return false;}// 发起连接成功return true;}// 获取文件描述符int sockfd() override{return _sockfd;}// 读取信息放到 out 中,并返回读取到的字符串的长度int Recv(string *out) override{// 从 sockfd 指定的文件中读取数据到 inbuffer 中char inbuffer[1024];ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if (n > 0){inbuffer[n] = 0;*out += inbuffer;}return n;}// 服务器将处理好后的字符串发送出去int Send(const string &in) override{int n = ::send(_sockfd, in.c_str(), in.size(), 0);return n;}// 地址复用void reuse_address() override{int opt = 1;::setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));}};
}

⭐ select_server 类

  • 在构造 select_server 对象时,需要指定 select 服务器的端口号。
  • 在初始化 select 服务器时,需要调用 Socket 类中的成员被函数,依次进行套接字的创建、绑定和监听。
  • 在析构函数中,可选择调用 close 函数将监听套接字关闭。
#pragma once#include <iostream>
#include <algorithm>
#include <sys/select.h>#include "socket.h"using std::cerr;
using std::make_unique;
using std::max;
using std::unique_ptr;using namespace socket_ns;class select_server
{const static int fd_max_num = sizeof(fd_set) * 8; // 文件描述符的最大数量 = fd_set 的大小const static int defauld_fd = -1;private:uint16_t _port;unique_ptr<Socket> _listen_socket;// select 想要正常工作,需要一个辅助数组,用来保存所有合法的 fdint fd_array[fd_max_num];public:select_server(uint16_t port): _port(port), _listen_socket(make_unique<tcp_socket>()){// 创建监听套接字_listen_socket->build_listen_socket(_port);}// 初始化服务器void init_server(){for (size_t i = 0; i < fd_max_num; i++)fd_array[i] = defauld_fd;fd_array[0] = _listen_socket->sockfd(); // 将 listen 套接字先添加进这个数组(默认用 0 号下标存储)}// 获取连接(处理 listen fd 的就绪事件)void accept_connection(){// 有新连接到来 - 连接事件就绪// 处理 listen 套接字Inet_addr addr;int sockfd = _listen_socket->accepter(&addr); // 由于 select 的原因,一定不会阻塞if (sockfd > 0){LOG(DEBUG, "get a new link success, client info: %s:%d", addr.ip().c_str(), addr.port());// 已经获得了一个新的 sockfd,但是接下来 【绝对不能 - 直接读取】// 这个新的 sockfd 不一定有数据可读,如果直接开始读,则直接进入阻塞 - 挂起 - 服务端挂掉// 只能通过 select 来判断底层 fd 是否有数据可读// 要想办法把新的 fd 添加给 select,由 select 统一监管// 这也就是为什么被 select 所监管的文件描述符越来越多的原因// 只要将新的 fd 添加到 fd_array 即可bool sockfd_add_to_fdarray_success = false;for (int pos = 1; pos < fd_max_num; pos++){// 找到一个空闲的位置, 将新的 sockfd 添加进去if (fd_array[pos] == defauld_fd){sockfd_add_to_fdarray_success = true;fd_array[pos] = sockfd;LOG(INFO, "add %d to fd_array success", sockfd);break;}}// 如果整个 fd_array 中都没位置放入新的 sockfdif (!sockfd_add_to_fdarray_success){LOG(WARNING, "server sockfd is full");close(sockfd);}}}// 处理 IO(处理普通 fd 的就绪事件)void handle_io(int i){// 处理普通 sockfd,正常读写char buffer[1024];// 这里的读取不会阻塞,因为 select 已经判断出 fd 的读事件就绪ssize_t n = ::recv(fd_array[i], buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = '\0';cout << "client say: " << buffer << endl;// 服务器给客户端回复string content = "<html><body><h1>hello world</h1></body></html>";string echo_str = "HTTP/1.0 200 OK\r\n";echo_str += "Content-Type: text/html\r\n";echo_str += "Content-Length: " + to_string(content.size()) + "\r\n\r\n";echo_str += content;// 发送数据::send(fd_array[i], echo_str.c_str(), echo_str.size(), 0);}else if (0 == n){// 对端关闭连接LOG(INFO, "client quit, sockfd: %d", fd_array[i]);close(fd_array[i]);fd_array[i] = defauld_fd; // 让 select 不要再关心这个 fd 了}else{// 读取失败LOG(ERROR, "recv error, sockfd: %d", fd_array[i]);close(fd_array[i]);fd_array[i] = defauld_fd;}}// 处理已经就绪的事件 (一定会同时存在大量就绪的 fd,可能是普通 sockfd,也可能是 listensockfd)void handle_event(fd_set &rfdset){for (size_t i = 0; i < fd_max_num; i++){if (fd_array[i] == defauld_fd)continue;cout << "for debug" << endl;// 能走到这里说明该 fd 一定合法(但不一定就绪),需要判断该 fd 是否就绪if (FD_ISSET(fd_array[i], &rfdset)){cout << "legal" << endl;// 读事件就绪// 需要判断是 listensockfd 还是 sockfd 就绪if (fd_array[i] == _listen_socket->sockfd())accept_connection();    // 处理连接事件else    handle_io(i);           // 处理普通事件}}}// 展示所有合法的 fdvoid print_all_legal_fd(){cout << "legal fd list: ";for (size_t i = 0; i < fd_max_num; i++)if (fd_array[i] != defauld_fd)cout << fd_array[i] << " ";cout << endl;}// 运行服务器void loop(){while (true){// 1.初始化文件描述符集fd_set readset;FD_ZERO(&readset); // 清空文件描述符集int max_fd = defauld_fd;// 2.将合法的文件描述符添加到文件描述符集中for (size_t i = 0; i < fd_max_num; i++){if (fd_array[i] == defauld_fd)continue;FD_SET(fd_array[i], &readset);// 更新出最大的文件描述符值if (max_fd < fd_array[i])max_fd = fd_array[i];}struct timeval timeout = {3, 0}; // 每隔 3s timeout 一次// 让 select 监听,此处只关心读事件是否就绪int n = ::select(max_fd + 1, &readset, nullptr, nullptr, nullptr /*&timeout*/);switch (n){case 0:LOG(DEBUG, "timeout, %d.%d", timeout.tv_sec, timeout.tv_usec);break;case -1:LOG(ERROR, "select error");break;default:LOG(INFO, "haved event ready, n: %d", n); // 如果事件就绪,但时不处理,select 会一直通知我,直到事件被处理为止handle_event(readset);print_all_legal_fd();sleep(1);break;}}}~select_server(){if (_listen_socket)close(_listen_socket->sockfd());}
};
%d.%d", timeout.tv_sec, timeout.tv_usec);break;case -1:LOG(ERROR, "select error");break;default:LOG(INFO, "haved event ready, n: %d", n); // 如果事件就绪,但时不处理,select 会一直通知我,直到事件被处理为止handle_event(readset);print_all_legal_fd();sleep(1);break;}}}~select_server(){if (_listen_socket)close(_listen_socket->sockfd());}
};

http://www.ppmy.cn/server/158659.html

相关文章

Win32汇编学习笔记11.游戏辅助的实现

Win32汇编学习笔记11.游戏辅助的实现-C/C基础-断点社区-专业的老牌游戏安全技术交流社区 - BpSend.net 游戏基址 游戏基址的概念 游戏基址是保持恒定的两部分内存地址的一部分并提供一个基准点&#xff0c;从这里可以计算一个字节数据的位置。基址伴随着一个加到基上的偏移值…

Postman 接口测试平替工具,可视化开发省事!

在软件开发的漫长旅程中&#xff0c;接口测试工具一直是开发者的得力助手。Postman 作为全球知名的接口测试工具&#xff0c;长期占据市场主导地位。然而&#xff0c;随着国产工具的崛起&#xff0c;越来越多的开发者开始寻找更适合中国开发者的替代方案。一款 Apifox&#xff…

uniapp区域滚动——上划进行分页加载数据(详细教程)

##标题 用来总结和学习&#xff0c;便于自己查找 文章目录 一、为什么scroll-view?          1.1 区域滚动页面滚动&#xff1f;          1.2 代码&#xff1f; 二、分页功能&#xff1f;          2.1 如何实现&#xff…

使用Python和Neo4j驱动程序来实现小规模数据的CSV导入

要将CSV数据导入到Neo4j数据库中&#xff0c;你可以使用Neo4j提供的工具&#xff0c;比如neo4j-admin import命令&#xff08;适用于大规模数据导入&#xff09;&#xff0c;或者使用Python的Neo4j驱动程序通过Cypher查询逐行插入数据&#xff08;适用于小规模数据导入&#xf…

2025年第三届“华数杯”国际赛A题解题思路与代码(Python版)

游泳竞技策略优化模型代码详解 第一题&#xff1a;速度优化模型 在这一部分&#xff0c;我们将详细解析如何通过数学建模来优化游泳运动员在不同距离比赛中的速度分配策略。 1. 模型概述 我们的模型主要包含三个核心文件&#xff1a; speed_optimization.py: 速度优化的核…

37_Lua运算符

运算符是一个特殊的符号,用于告诉解释器执行特定的数学或逻辑运算。Lua支持多种运算符,包括: 算术运算符关系运算符逻辑运算符赋值运算符其他运算符1.算术运算符 下表列出了Lua语言中的常用的算术运算符,设定A的值为10,B的值为20。 运算符 运算 示例 结果 + 加法,将两个…

【Docker】Docker部署多种容器

关于docker&#xff0c;Windows上使用Powershell/CMD执行指令&#xff0c;Linux系统直接使用终端执行指令。 docker安装MySQL 拉取MySQL 也可以跳过拉取步骤&#xff0c;直接run&#xff0c;这样本地容器不存在的话&#xff0c;会自动拉取最新/指定的版本。 # 默认拉取最新…

软件设计模式的原则

【单一原则】&#xff08;Single Responsibility Principle&#xff09;&#xff1a;一个类或者一个方法只负责一项职责。 【里氏替换原则】&#xff08;LSP liskov substitution principle&#xff09;&#xff1a;子类可以扩展父类的功能&#xff0c;但不能改变原有父类的功…