poll

poll使用相对较少,跨平台推荐select,高并发推荐epoll

一、poll 核心概念

poll 是 Linux 下另一种多路复用 I/O 模型,与你之前学习的 select 功能类似(监控多个文件描述符的事件),但解决了 select 的部分缺陷,是网络编程中实现多客户端并发的常用方案。

1.1 与 select 的核心区别(对比你的 select 代码)

特性 select poll
文件描述符集合存储 固定大小的位图(fd_set) 动态的结构体数组(pollfd)
最大监控描述符限制 受限于 FD_SETSIZE(默认 1024) 无硬限制,仅受系统资源约束
事件重置 每次需重新初始化集合 无需重置,事件结果存在 revents 中
参数传递 值传递(会修改原集合) 结构体数组(输入输出分离)

1.2 核心优势

  • 突破 select 最大 1024 个文件描述符的限制;
  • 无需像 select 那样每次循环重置文件描述符集合;
  • 支持更多类型的事件监控(如普通读、带外数据读、出错等)。

二、poll 核心 API 详解

2.1 头文件

1
#include <poll.h>  // poll的核心头文件

2.2 pollfd 结构体(核心)

poll 通过结构体数组管理要监控的文件描述符和事件,替代 selectfd_set

1
2
3
4
5
struct pollfd {
int fd; // 要监控的文件描述符(如监听套接字lfd、客户端套接字cfd)
short events; // 要监控的事件(输入:告诉poll要监听什么)
short revents; // 实际发生的事件(输出:poll返回后,内核填充)
};

常用事件宏(events/revents 取值)

事件宏 含义 适用场景
POLLIN 有数据可读(读事件) 监听新连接、客户端发数据
POLLOUT 可写数据(写事件) 向客户端发送大量数据
POLLERR 出错事件 检测套接字错误
POLLHUP 挂起事件(客户端断开连接) 检测客户端主动关闭

2.3 poll 函数

1
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明

  1. fdspollfd 结构体数组的首地址,存放要监控的所有文件描述符及事件;

  2. nfds:数组中有效元素的个数(需指定监控的文件描述符数量);

  3. timeout:超时时间(毫秒):

    • -1:永久阻塞,直到有事件触发(同 select 的 NULL);
    • 0:非阻塞,立即返回,不管是否有事件;
    • >0:阻塞指定毫秒数,超时后返回 0。

返回值

  • >0:触发事件的文件描述符总数;
  • 0:超时,无事件触发;
  • -1:调用失败(如被信号中断),设置 errno

三、基于 poll 实现 TCP 回显服务器(对标你的 select 代码)

3.1 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<string.h>
#include<poll.h> // 替换 select 头文件

#define MAX_CLIENT 1024 // 最大客户端数(可根据需求调整)

int main()
{
// 1. 创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);

// 2. 初始化服务器地址
struct sockaddr_in caddr;
caddr.sin_family = AF_INET;
caddr.sin_port = htons(10000);
caddr.sin_addr.s_addr = INADDR_ANY;

// 3. 绑定
int bind_res = bind(lfd, (struct sockaddr*)&caddr, sizeof(caddr));
if (bind_res == -1) {
perror("bind failed");
close(lfd);
return -1;
}

// 4. 监听
listen(lfd, 128);

// 5. 初始化 pollfd 数组
struct pollfd fds[MAX_CLIENT] = {0};
fds[0].fd = lfd; // 第一个元素监控监听套接字
fds[0].events = POLLIN; // 监控读事件(新连接)
// 初始化其他元素:fd=-1 表示未使用
for (int i = 1; i < MAX_CLIENT; i++) {
fds[i].fd = -1;
}
int nfds = 0; // 有效监控的文件描述符个数(初始只有lfd)

while(1)
{
// 6. 调用 poll 监控事件
int ret = poll(fds, MAX_CLIENT, -1); // 永久阻塞
if (ret == -1) {
perror("poll failed");
continue;
}

// 7. 处理新连接(监听套接字触发 POLLIN)
if (fds[0].revents & POLLIN) {
struct sockaddr_in caddr;
int caddr_len = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr*)&caddr, &caddr_len);

// 找到数组中第一个未使用的位置(fd=-1)
int i;
for (i = 1; i < MAX_CLIENT; i++) {
if (fds[i].fd == -1) {
fds[i].fd = cfd;
fds[i].events = POLLIN; // 监控客户端读事件
break;
}
}
// 更新有效监控数(取最大的索引)
if (i > nfds) {
nfds = i;
}
}

// 8. 处理客户端数据
for(int i = 1; i <= nfds; ++i){
// 跳过未使用的位置
if (fds[i].fd == -1) {
continue;
}
// 检查是否触发读事件/出错/挂起
if (fds[i].revents & (POLLIN | POLLERR | POLLHUP)) {
char buffer[10]; // 保留你的10字节缓冲区
int len = read(fds[i].fd, buffer, sizeof(buffer));
if(len > 0){
// 回显数据
write(fds[i].fd, buffer, len);
}
else if(len == 0 || (fds[i].revents & POLLHUP)){
// 客户端关闭连接
printf("客户端关闭连接...\n");
close(fds[i].fd);
fds[i].fd = -1; // 标记为未使用
}
else{
perror("read");
close(fds[i].fd);
fds[i].fd = -1;
}
}
}
}

close(lfd);
return 0;
}

3.2 核心逻辑对比

步骤 select 实现 poll 实现
监控集合初始化 FD_ZERO/FD_SET 操作 fd_set 初始化 pollfd 数组,fd=-1 标记未使用
事件监控 select(maxfd+1, &rdtemp, …) poll(fds, nfds, -1)
新连接判断 FD_ISSET(lfd, &rdtemp) fds[0].revents & POLLIN
客户端事件判断 FD_ISSET(i, &rdtemp) fds[i].revents & POLLIN
资源清理 FD_CLR(i, &rdset) + close fds[i].fd=-1 + close