【在Linux世界中追寻伟大的One Piece】多路转接select

devtools/2024/11/9 0:20:19/

目录

1 -> I/O多路转接之select

1.1 -> 初识select

1.2 -> select函数原型

1.3 -> 关于fd_set结构

1.4 -> 关于timeval结构

2 -> 理解select执行过程

2.1 -> Socket就绪条件

2.2 -> select特点

2.3 -> select缺点

3 -> select使用示例

3.1 -> 只检测标准输入

3.2 -> 实现字典服务器


1 -> I/O多路转接之select

1.1 -> 初识select

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

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

1.2 -> 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将超时返回。

1.3 -> 关于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 的全部位

1.4 -> 关于timeval结构

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

函数返回值:

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

错误值可能为:

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

常见的程序片段如下:

fs_set readset;
FD_SET(fd, &readset);
select(fd + 1, &readset, NULL, NULL, NULL);
if (FD_ISSET(fd, readset)) { …… }

2 -> 理解select执行过程

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

  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被清空。

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上有未读取的错误。

2.2 -> select特点

  • 可监控的文件描述符个数取决于sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd。
    • 一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。
    • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

2.3 -> select缺点

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

3 -> select使用示例

3.1 -> 只检测标准输入

#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);for (;;) {printf("> ");fflush(stdout);int ret = select(1, &read_fds, NULL, NULL, NULL);if (ret < 0) {perror("select");continue;}if (FD_ISSET(0, &read_fds)) {char buf[1024] = { 0 };read(0, buf, sizeof(buf) - 1);printf("input: %s", buf);}else {printf("error! invaild fd\n");continue;}FD_ZERO(&read_fds);FD_SET(0, &read_fds);}return 0;
}

说明:

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

3.2 -> 实现字典服务器

tcp_select_server.hpp

#pragma once#include <vector>
#include <unordered_map>
#include <functional>
#include <sys/select.h>
#include "tcp_socket.hpp"// 必要的调试函数
inline void PrintFdSet(fd_set* fds, int max_fd) 
{printf("select fds: ");for (int i = 0; i < max_fd + 1; ++i) {if (!FD_ISSET(i, fds)) {continue;}printf("%d ", i);}printf("\n");
}typedef std::function<void(const std::string& req, std::string*resp)> Handler;// 把 Select 封装成一个类. 这个类虽然保存很多 TcpSocket 对象指针, 但是不管理内存
class Selector 
{
public:Selector() {// [注意!] 初始化千万别忘了!!max_fd_ = 0;FD_ZERO(&read_fds_);}bool Add(const TcpSocket& sock) {int fd = sock.GetFd();printf("[Selector::Add] %d\n", fd);if (fd_map_.find(fd) != fd_map_.end()) {printf("Add failed! fd has in Selector!\n");return false;}fd_map_[fd] = sock;FD_SET(fd, &read_fds_);if (fd > max_fd_) {max_fd_ = fd;}return true;}bool Del(const TcpSocket& sock) {int fd = sock.GetFd();printf("[Selector::Del] %d\n", fd);if (fd_map_.find(fd) == fd_map_.end()) {printf("Del failed! fd has not in Selector!\n");return false;}fd_map_.erase(fd);FD_CLR(fd, &read_fds_);// 重新找到最大的文件描述符, 从右往左找比较快for (int i = max_fd_; i >= 0; --i) {if (!FD_ISSET(i, &read_fds_)) {continue;}max_fd_ = i;break;}return true;}// 返回读就绪的文件描述符集bool Wait(std::vector<TcpSocket>* output) {output->clear();// [注意] 此处必须要创建一个临时变量, 否则原来的结果会被覆盖掉fd_set tmp = read_fds_;// DEBUGPrintFdSet(&tmp, max_fd_);int nfds = select(max_fd_ + 1, &tmp, NULL, NULL, NULL);if (nfds < 0) {perror("select");return false;}// [注意!] 此处的循环条件必须是 i < max_fd_ + 1for (int i = 0; i < max_fd_ + 1; ++i) {if (!FD_ISSET(i, &tmp)) {continue;}output->push_back(fd_map_[i]);}return true;}private:fd_set read_fds_;int max_fd_;// 文件描述符和 socket 对象的映射关系std::unordered_map<int, TcpSocket> fd_map_;
};class TcpSelectServer 
{
public:TcpSelectServer(const std::string& ip, uint16_t port) : ip_(ip),port_(port) {}bool Start(Handler handler) const {// 1. 创建 socketTcpSocket listen_sock;bool ret = listen_sock.Socket();if (!ret) {return false;}// 2. 绑定端口号ret = listen_sock.Bind(ip_, port_);if (!ret) {return false;}// 3. 进行监听ret = listen_sock.Listen(5);if (!ret) {return false;}// 4. 创建 Selector 对象Selector selector;selector.Add(listen_sock);// 5. 进入事件循环for (;;) {std::vector<TcpSocket> output;bool ret = selector.Wait(&output);if (!ret) {continue;}// 6. 根据就绪的文件描述符的差别, 决定后续的处理逻辑for (size_t i = 0; i < output.size(); ++i) {if (output[i].GetFd() == listen_sock.GetFd()) {// 如果就绪的文件描述符是 listen_sock, 就执行 accept, 并加入到 select 中TcpSocket new_sock;listen_sock.Accept(&new_sock, NULL, NULL);selector.Add(new_sock);}else {// 如果就绪的文件描述符是 new_sock, 就进行一次请求的处理std::string req, resp;bool ret = output[i].Recv(&req);if (!ret) {selector.Del(output[i]);// [注意!] 需要关闭 socketoutput[i].Close();continue;}// 调用业务函数计算响应handler(req, &resp);// 将结果写回到客户端output[i].Send(resp);}} // end for} // end for (;;)return true;}private:std::string ip_;uint16_t port_;
};

感谢各位大佬支持!!!

互三啦!!!


http://www.ppmy.cn/devtools/132435.html

相关文章

FPGA 实现CAN通信

使用FPGASJA 1000芯片实现CAN通信。核心思路是对集成CAN协议的芯片尽心配置&#xff0c;来进行CAN通信。核心顶层代码&#xff1a; //-- Company: //-- Engineer: //-- //-- Create Date: 11:18:25 12/01/2021 //-- Design Name: //-- Module Name: con_port - …

Unity性能优化 -- 性能分析工具

Stats窗口Profiler窗口Memory Profiler其他性能分析工具&#xff08;Physica Debugger 窗口&#xff0c;Import Activity 窗口&#xff0c;Code Coverage 窗口&#xff0c;Profile Analyzer 窗口&#xff0c;IMGUI Debugger 窗口&#xff09; Stats 统级数据窗口 game窗口 可…

用示例来看C2Rust工具的使用和功能介绍

C2Rust可以将C语言的源代码转换成Rust语言的源代码。下面是一个简单的C语言代码示例,以及使用c2Rust工具将其转换为Rust安全代码的过程。 C语言源代码示例 // example.c #include <stdio.h>int add(int a, int b)

leetcode203. Remove Linked List Elements

给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 Given the head of a linked list and an integer val, remove all the nodes of the linked list that has Node.val val, and return …

git原理与上传

言&#xff1a; git是一个软件&#xff0c;gitee/github是一个网站&#xff0c;这里有什么联系吗&#xff1f;我们身为一个程序员不可能不知道github&#xff0c;但是毕竟这是外国的网站&#xff0c;我们不翻墙的情况下&#xff0c;是无法访问的(或者就是太慢了&#xff0c;或…

Solana 代币 2022 — Transfer Hook

从零到英雄的 Solana 代币 2022 — Transfer Hook Token 2022 计划引入了几项令人兴奋的扩展&#xff0c;增强了铸造和代币账户的功能。在这些功能中&#xff0c;我个人最喜欢的是Transfer Hook &#xff08;转账钩子&#xff09; 。 想象时间 让我们戴上想象的帽子&#xf…

【系统架构设计师】高分论文:论软件的可用性设计

更多内容请见: 备考系统架构设计师-专栏介绍和目录 文章目录 摘要正文摘要 2021年5月,我参加了某市人才集团信息化集中项目的建设。在该项目中,我担任系统架构师。该项目合同金额为 523.5 万元,建设工期为8个月,项目建设内容包含新建一个门户网站、新建4个子系统以及集成…

腾讯为什么支持开源?

今天看到一条新闻&#xff0c;感觉腾讯在 AI 大模型方面确实挺厉害的&#xff0c;符合它低调务实的风格&#xff0c;在不知不觉中一天竟然开源了两个核心的&#xff0c;重要的 AI 大模型。 据新闻报道&#xff0c;11月 5 日&#xff0c;腾讯混元宣布最新的 MoE 模型“混元 Larg…