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

高级IO-poll

目录

一、为什么需要 poll?select 的痛点回顾

二、poll 核心原理与数据结构

1. 核心结构:struct pollfd

2. poll 函数原型

三、实战:基于 poll 实现 TCP 服务器

1. 类结构与初始化

2. 处理新连接:Accepter 方法

3. 接收客户端数据:Recver 方法

4. 事件分发:Dispatcher 方法

5. 启动服务器:Start 方法

四、poll 对比 select:优势与局限

优势:

局限:

五、总结:poll 适合什么场景?


在网络编程中,多路复用技术是处理并发连接的基石。上一篇我们探讨了 select 的实现,但其固有的 fd 数量限制和重复初始化问题始终是瓶颈。本文将聚焦 poll 机制 —— 它作为 select 的改进版,解决了不少痛点。我们将通过一个完整的 poll 服务器实现,深入理解其工作原理与优势。

一、为什么需要 poll?select 的痛点回顾

select 作为早期的多路复用方案,存在三个明显缺陷:

  1. fd 数量上限:由 fd_set 位图长度决定(通常默认 1024),无法灵活扩展。
  2. 输入输出参数混合:每次调用 select 都需重新初始化 fd_set,重复劳动且效率低。
  3. 遍历成本高:用户态和内核态都需遍历全部监控 fd 才能确定就绪事件。

poll 的出现正是为了针对性解决这些问题,尤其是前两点。

二、poll 核心原理与数据结构

1. 核心结构:struct pollfd

poll 不再使用位图,而是通过一个结构体数组管理文件描述符(fd)和事件,结构体定义如下:

struct pollfd {int fd;         // 待监控的文件描述符short events;   // 输入:用户关心的事件(如 POLLIN 表示可读)short revents;  // 输出:内核返回的实际就绪事件
};
  • 分离输入输出events 仅用于设置监控需求,revents 用于返回结果,无需每次重置。
  • 事件类型清晰:支持 POLLIN(可读)、POLLOUT(可写)、POLLERR(错误)等事件,与 select 功能类似但表达更直接。

2. poll 函数原型

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fdsstruct pollfd 数组,存放待监控的 fd 及事件。
  • nfds:数组长度(需监控的 fd 数量)。
  • timeout:超时时间(毫秒):
    • timeout > 0:阻塞等待指定毫秒数。
    • timeout = 0:非阻塞,立即返回。
    • timeout = -1:无限期阻塞,直到有事件就绪。
  • 返回值:就绪事件的总数(失败返回 -1,超时返回 0)。

三、实战:基于 poll 实现 TCP 服务器

下面通过代码实现一个完整的 poll 服务器,感受其与 select 的差异。

1. 类结构与初始化

#pragma once
#include <iostream>
#include <poll.h>
#include "Socket.hpp"  // 自定义套接字封装类using namespace std;static const uint16_t defaultport = 8888;
static const int fd_num_max = 64;  // 可自定义的fd上限(比select更灵活)
int defaultfd = -1;                // 标记未使用的fd
int non_event = 0;                 // 无事件标记class PollServer {
public:PollServer(uint16_t port = defaultport) : _port(port) {// 初始化pollfd数组:所有fd设为-1(未使用),事件设为0for (int i = 0; i < fd_num_max; i++) {_event_fds[i].fd = defaultfd;_event_fds[i].events = non_event;_event_fds[i].revents = non_event;}}bool Init() {// 初始化监听套接字:创建、绑定、监听_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();return true;}// ... 其他方法后续展开
private:Sock _listensock;               // 监听套接字uint16_t _port;                 // 服务器端口struct pollfd _event_fds[fd_num_max];  // pollfd数组,管理所有监控的fd
};
  • 与 select 不同,poll 直接用 struct pollfd 数组管理 fd,无需单独维护 fd 列表,结构更紧凑。

2. 处理新连接:Accepter 方法

当监听套接字的 POLLIN 事件就绪时,接收新连接并将客户端 fd 加入 _event_fds 数组。

void Accepter() {std::string clientip;uint16_t clientport = 0;int sock = _listensock.Accept(&clientip, &clientport);  // 非阻塞,因poll已通知就绪if (sock < 0) return;lg(Info, "accept success, %s:%d, sock fd:%d", clientip.c_str(), clientport, sock);// 找一个空闲位置存储新客户端fdint pos = 1;  // 位置0留给监听套接字for (; pos < fd_num_max; pos++) {if (_event_fds[pos].fd != defaultfd) continue;else break;}if (pos == fd_num_max) {  // 服务器fd已满lg(Warning, "server is full, close %d now!", sock);close(sock);} else {  // 加入监控,关注可读事件_event_fds[pos].fd = sock;_event_fds[pos].events = POLLIN;  // 仅设置一次,无需每次重置_event_fds[pos].revents = non_event;PrintFd();  // 打印当前在线fd}
}
  • 关键差异:events 只需初始化时设置一次,后续 poll 调用会复用,无需像 select 那样每次清空重设。

3. 接收客户端数据:Recver 方法

当客户端 fd 的 POLLIN 事件就绪时,读取数据并处理连接关闭 / 错误场景。

void Recver(int fd, int pos) {char buffer[1024];ssize_t n = read(fd, buffer, sizeof(buffer) - 1);if (n > 0) {  // 读取成功buffer[n] = 0;cout << "get a message: " << buffer << endl;} else if (n == 0) {  // 客户端断开lg(Info, "client quit, close fd: %d", fd);close(fd);_event_fds[pos].fd = defaultfd;  // 标记为未使用(从监控中移除)} else {  // 读取错误lg(Warning, "recv error, fd: %d", fd);close(fd);_event_fds[pos].fd = defaultfd;}
}

4. 事件分发:Dispatcher 方法

遍历 _event_fds 数组,通过 revents 检查就绪事件,分发给对应处理函数。

void Dispatcher() {for (int i = 0; i < fd_num_max; i++) {int fd = _event_fds[i].fd;if (fd == defaultfd) continue;// 检查内核返回的可读事件if (_event_fds[i].revents & POLLIN) {if (fd == _listensock.Fd()) {  // 监听套接字:新连接Accepter();} else {  // 客户端套接字:数据可读Recver(fd, i);}}}
}
  • 与 select 相比,poll 通过 revents 直接返回就绪事件,无需调用 FD_ISSET 宏,代码更直观。

5. 启动服务器:Start 方法

主循环中调用 poll 监控事件,就绪后通过 Dispatcher 处理。

void Start() {// 监听套接字加入监控,关注可读事件(新连接)_event_fds[0].fd = _listensock.Fd();_event_fds[0].events = POLLIN;int timeout = 3000;  // 超时时间3秒for (;;) {// 调用poll监控事件,无需每次重置eventsint n = poll(_event_fds, fd_num_max, timeout);switch (n) {case 0:  // 超时cout << "time out... " << endl;break;case -1:  // 错误cerr << "poll error" << endl;break;default:  // 有事件就绪cout << "get a new event!!!!!" << endl;Dispatcher();break;}}
}
  • 核心优势:poll 调用时无需重新初始化 _event_fds 数组,events 字段保持不变,减少重复操作。

四、poll 对比 select:优势与局限

优势:

  1. 突破 fd 数量上限select 依赖 fd_set 位图长度,而 poll 的 fd 数量由数组大小决定(可自定义,理论上仅受系统最大 fd 限制)。
  2. 输入输出分离events(输入)和 revents(输出)分离,无需每次重置事件集,减少代码冗余。
  3. 无需计算 maxfdselect 需要传入最大 fd + 1,poll 直接传入数组长度,更简洁。

局限:

  1. 遍历开销仍存在:与 select 一样,poll 返回后仍需遍历整个数组才能找到就绪的 fd,当 fd 数量庞大时效率下降。
  2. 数据拷贝开销:每次调用 poll 仍需将整个 pollfd 数组拷贝到内核空间,fd 越多拷贝成本越高。

完整代码:

#pragma once#include <iostream>
#include <poll.h>
#include <sys/time.h>
#include "Socket.hpp"using namespace std;static const uint16_t defaultport = 8888;
static const int fd_num_max = 64;
int defaultfd = -1;
int non_event = 0;class PollServer
{
public:PollServer(uint16_t port = defaultport) : _port(port){for (int i = 0; i < fd_num_max; i++){_event_fds[i].fd = defaultfd;_event_fds[i].events = non_event;_event_fds[i].revents = non_event;// std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;}}bool Init(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();return true;}void Accepter(){// 我们的连接事件就绪了std::string clientip;uint16_t clientport = 0;int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会if (sock < 0) return;lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);// sock -> fd_array[]int pos = 1;for (; pos < fd_num_max; pos++) // 第二个循环{if (_event_fds[pos].fd != defaultfd)continue;elsebreak;}if (pos == fd_num_max){lg(Warning, "server is full, close %d now!", sock);close(sock);// 扩容}else{// fd_array[pos] = sock;_event_fds[pos].fd = sock;_event_fds[pos].events = POLLIN;_event_fds[pos].revents = non_event;PrintFd();// TODO}}void Recver(int fd, int pos){// demochar buffer[1024];ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?if (n > 0){buffer[n] = 0;cout << "get a messge: " << buffer << endl;}else if (n == 0){lg(Info, "client quit, me too, close fd is : %d", fd);close(fd);_event_fds[pos].fd = defaultfd; // 这里本质是从select中移除}else{lg(Warning, "recv error: fd is : %d", fd);close(fd);_event_fds[pos].fd = defaultfd; // 这里本质是从select中移除}}void Dispatcher(){for (int i = 0; i < fd_num_max; i++) // 这是第三个循环{int fd = _event_fds[i].fd;if (fd == defaultfd)continue;if (_event_fds[i].revents & POLLIN){if (fd == _listensock.Fd()){Accepter(); // 连接管理器}else // non listenfd{Recver(fd, i);}}}}void Start(){_event_fds[0].fd = _listensock.Fd();_event_fds[0].events = POLLIN;int timeout = 3000; // 3sfor (;;){int n = poll(_event_fds, fd_num_max, timeout);switch (n){case 0:cout << "time out... " << endl;break;case -1:cerr << "poll error" << endl;break;default:// 有事件就绪了,TODOcout << "get a new link!!!!!" << endl;Dispatcher(); // 就绪的事件和fd你怎么知道只有一个呢???break;}}}void PrintFd(){cout << "online fd list: ";for (int i = 0; i < fd_num_max; i++){if (_event_fds[i].fd == defaultfd)continue;cout << _event_fds[i].fd << " ";}cout << endl;}~PollServer(){_listensock.Close();}private:Sock _listensock;uint16_t _port;struct pollfd _event_fds[fd_num_max]; // 数组, 用户维护的!// struct pollfd *_event_fds;// int fd_array[fd_num_max];// int wfd_array[fd_num_max];
};

五、总结:poll 适合什么场景?

poll 是 select 的优化版本,解决了 fd 数量限制和事件集重复初始化问题,在中小并发场景(如 fd 数量 1000-10000)中表现优于 select。但它并未彻底解决遍历和数据拷贝的开销,因此在高并发(如十万级连接)场景中,仍需依赖 epoll(Linux)或 kqueue(BSD)等更高效的机制。

理解 poll 的设计思路 —— 通过结构体数组分离输入输出、灵活扩展 fd 数量 —— 是掌握多路复用技术演进的关键一步。下一篇我们将探讨 epoll,看看它如何进一步突破 poll 的瓶颈。

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

相关文章:

  • 在JavaScript中,JavaScript 对象和 JSON 字符串互相转换
  • css之弹性盒子属性2
  • [LivePortrait] docs | Gradio用户界面
  • 基于C#+avalonia ui实现的跨平台点胶机灌胶监控控制上位机软件
  • 【三维编辑】DREAMCATALYST:平衡可编辑性和ID的快速高效3D编辑
  • SemanticVLA:面向高效机器人操作的语义对齐剪枝与增强方法
  • 晋城市住建设局网站茂名公司网站设计
  • 太原做响应式网站软件开发公司厂家有哪些
  • 40_FastMCP 2.x 中文文档之FastMCP客户端认证:OAuth 身份验证详解
  • 二、Rabbit MQ 高级
  • 66-69 原型对象,toString(),垃圾回收
  • Node 的版本管理工具 nvm 介绍
  • ubuntu24.04搭建GitLab服务器
  • 前端 vs 后端:入行软件行业,我该如何选择?哪个更“简单”?
  • 网站产品策划中国建设银行遵义市分行网站
  • 【自适应卡尔曼滤波】(EKF、UKF、CKF等可通用)的创新思路:哪些参数该使用自适应思想来调整、该怎么调
  • RHCSE--SELinux
  • 安装k8s过程中涉及知识点梳理
  • 公司网站建设费用如何做账英语可以做推广的亲子类网站
  • 【2025】Java 从入门到实战:核心特性全解析(方法、类与对象、多态三大特性及关键字thissuper)附带代码案例
  • FSMC-TFTLCD显示实验(1)
  • Node.js 实现 Stripe 支付的简单示例
  • Claude、Agent与Copilot协作生成Angular应用
  • 建行广东茂名茂南支行:积极走进会展商圈,助力金融赋能消费提振
  • LDO(Low Dropout Regulator)是什么?
  • 专门做win7系统的网站免费金融发布网站模板
  • 网络安全:SQL 注入:SQLmap
  • UI设计公司审美积累|APP界面从风格到功能的设计智慧
  • 分布式专题——57 如何保证MySQL数据库到ES的数据一致性
  • 厦门建站公司哪家好wordpress 虚拟商城