目录
[一、为什么需要 poll?select 的痛点回顾](#一、为什么需要 poll?select 的痛点回顾)
[二、poll 核心原理与数据结构](#二、poll 核心原理与数据结构)
[1. 核心结构:struct pollfd](#1. 核心结构:struct pollfd)
[2. poll 函数原型](#2. poll 函数原型)
[三、实战:基于 poll 实现 TCP 服务器](#三、实战:基于 poll 实现 TCP 服务器)
[1. 类结构与初始化](#1. 类结构与初始化)
[2. 处理新连接:Accepter 方法](#2. 处理新连接:Accepter 方法)
[3. 接收客户端数据:Recver 方法](#3. 接收客户端数据:Recver 方法)
[4. 事件分发:Dispatcher 方法](#4. 事件分发:Dispatcher 方法)
[5. 启动服务器:Start 方法](#5. 启动服务器:Start 方法)
[四、poll 对比 select:优势与局限](#四、poll 对比 select:优势与局限)
[五、总结:poll 适合什么场景?](#五、总结:poll 适合什么场景?)
在网络编程中,多路复用技术是处理并发连接的基石。上一篇我们探讨了 select 的实现,但其固有的 fd 数量限制和重复初始化问题始终是瓶颈。本文将聚焦 poll 机制 ------ 它作为 select 的改进版,解决了不少痛点。我们将通过一个完整的 poll 服务器实现,深入理解其工作原理与优势。
一、为什么需要 poll?select 的痛点回顾
select 作为早期的多路复用方案,存在三个明显缺陷:
- fd 数量上限 :由
fd_set位图长度决定(通常默认 1024),无法灵活扩展。 - 输入输出参数混合 :每次调用
select都需重新初始化fd_set,重复劳动且效率低。 - 遍历成本高:用户态和内核态都需遍历全部监控 fd 才能确定就绪事件。
poll 的出现正是为了针对性解决这些问题,尤其是前两点。
二、poll 核心原理与数据结构
1. 核心结构:struct pollfd
poll 不再使用位图,而是通过一个结构体数组管理文件描述符(fd)和事件,结构体定义如下:
cpp
struct pollfd {
int fd; // 待监控的文件描述符
short events; // 输入:用户关心的事件(如 POLLIN 表示可读)
short revents; // 输出:内核返回的实际就绪事件
};
- 分离输入输出 :
events仅用于设置监控需求,revents用于返回结果,无需每次重置。 - 事件类型清晰 :支持
POLLIN(可读)、POLLOUT(可写)、POLLERR(错误)等事件,与select功能类似但表达更直接。 
2. poll 函数原型
cpp
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:struct pollfd数组,存放待监控的 fd 及事件。nfds:数组长度(需监控的 fd 数量)。timeout:超时时间(毫秒):timeout > 0:阻塞等待指定毫秒数。timeout = 0:非阻塞,立即返回。timeout = -1:无限期阻塞,直到有事件就绪。
- 返回值:就绪事件的总数(失败返回 -1,超时返回 0)。
三、实战:基于 poll 实现 TCP 服务器
下面通过代码实现一个完整的 poll 服务器,感受其与 select 的差异。
1. 类结构与初始化
cpp
#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(未使用),事件设为0
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;
}
}
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 数组。
cpp
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);
// 找一个空闲位置存储新客户端fd
int 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 事件就绪时,读取数据并处理连接关闭 / 错误场景。
cpp
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 检查就绪事件,分发给对应处理函数。
cpp
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 处理。
cpp
void Start() {
// 监听套接字加入监控,关注可读事件(新连接)
_event_fds[0].fd = _listensock.Fd();
_event_fds[0].events = POLLIN;
int timeout = 3000; // 超时时间3秒
for (;;) {
// 调用poll监控事件,无需每次重置events
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: // 有事件就绪
cout << "get a new event!!!!!" << endl;
Dispatcher();
break;
}
}
}
- 核心优势:
poll调用时无需重新初始化_event_fds数组,events字段保持不变,减少重复操作。
四、poll 对比 select:优势与局限
优势:
- 突破 fd 数量上限 :
select依赖fd_set位图长度,而poll的 fd 数量由数组大小决定(可自定义,理论上仅受系统最大 fd 限制)。 - 输入输出分离 :
events(输入)和revents(输出)分离,无需每次重置事件集,减少代码冗余。 - 无需计算 maxfd :
select需要传入最大 fd + 1,poll直接传入数组长度,更简洁。
局限:
- 遍历开销仍存在 :与
select一样,poll返回后仍需遍历整个数组才能找到就绪的 fd,当 fd 数量庞大时效率下降。 - 数据拷贝开销 :每次调用
poll仍需将整个pollfd数组拷贝到内核空间,fd 越多拷贝成本越高。
完整代码:
cpp
#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;
else
break;
}
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)
{
// demo
char 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; // 3s
for (;;)
{
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:
// 有事件就绪了,TODO
cout << "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 的瓶颈。