IO多路复用之poll函数详解用法-带详细注释-阻塞版本
1. poll以及相关结构体pollfd
-
poll() 是一种 IO多路复用机制,它能让一个线程同时监听多个文件描述符(通常是多个 socket),等待它们“可读”“可写”“异常”等事件的发生。
-
poll()函数以及struct pollfd结构体介绍
#include <poll.h>
struct pollfd
{
int fd; // 要监听的文件描述符
short events; // 感兴趣的事件
short revents; // 返回的事件(由内核填写)
};
/**
* @brief 轮询由pollfd结构描述的文件描述符,从FDS。如果TIMEOUT为非零且非-1,则允许TIMEOUT毫秒即将发生的事件;
* 如果TIMEOUT为-1,则阻塞直到事件发生。返回带有事件的文件描述符的个数,如果超时则为零;或者-1表示错误。
*
* @param fds 要监听的文件描述符数组
* @param nfds 数组元素数量
* @param timeout 等待超时(毫秒),-1 表示无限等待,0 表示立即返回
* @return * int 返回带有事件的文件描述符的个数,如果超时则为零;或者-1表示错误。
*/
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
- pollfd里的events常用事件标志
| 事件常量 | 含义 | 说明 |
|---|---|---|
POLLIN | 可读事件 | 表示对应的 fd 可进行读取操作(包括普通数据与优先数据) |
POLLRDNORM | 普通数据可读 | 与 POLLIN 类似,表示普通数据可读 |
POLLRDBAND | 优先级带数据可读 | 通常用于带外数据(OOB) |
POLLPRI | 紧急数据到达 | 有紧急(优先级高)数据可读 |
POLLOUT | 可写事件 | 表示 fd 可立即写入而不会阻塞 |
POLLWRNORM | 普通数据可写 | 与 POLLOUT 类似 |
POLLWRBAND | 优先级带数据可写 | 一般用于带外数据 |
POLLERR | 错误事件 | 发生错误,例如连接异常、写入失败 |
POLLHUP | 挂起事件 | 对端关闭连接(写端关闭) |
POLLNVAL | 无效 fd | 文件描述符未打开或非法 |
- 相对于早期的io复用之select对比
| 对比项 | select | poll |
|---|---|---|
| 文件描述符上限 | 有固定上限(常见 1024) | 无固定上限(由系统资源决定) |
| 描述符集合存储结构 | 位图(fd_set) | 数组(pollfd) |
| 集合重建 | 每次调用都要重置 fd_set | 不需要重建,只需更新结构体内容 |
| 事件类型 | 可读、可写、异常 | 可读、可写、异常(更丰富) |
| 性能复杂度 | O(n)(扫描全部 fd) | O(n)(同样逐个扫描) |
| 兼容性 | 所有系统都有 | POSIX 标准,兼容性强 |
| 返回方式 | 修改原集合,仅保留就绪 fd | 在 revents 字段返回事件 |
| fd 检查方式 | 用宏 FD_ISSET() | 直接检查 revents 位掩码 |
| 适用场景 | 小规模连接(≤1K) | 中等规模连接(几千) |
| 推荐程度 | 仅用于教学或老项目 | 实际开发更常用(中等并发) |
2. 基础示例和整个数据流程:等待标准输入
- 流程:
- 程序调用 poll(&fds, 1, 10000),请求内核监控文件描述符 STDIN_FILENO的 可读事件(POLLIN),超时时间为 10 秒。
- 当你10s内输入数据并按下回车,你输入的数据将会发送到标准输入(STDIN_FILENO=0)的内核缓冲区(内核将输入数据暂存到 文件描述符 0 的接收缓冲区 中,等待程序读取
) - 事件触发:当用户输入数据并按下回车后,内核标记 STDIN_FILENO为 就绪状态(可读),并通知 poll函数返回。
- 程序检测到 fds[0].revents & POLLIN为真,调用 read(STDIN_FILENO, buf, sizeof(buf)-1)读取数据
#include <iostream>
#include <poll.h>
#include <unistd.h>
int main()
{
struct pollfd fds[1];
fds[0].fd = STDIN_FILENO; // 标准输入
fds[0].events = POLLIN; // 关心可读事件
std::cout << "请输入内容 (10秒内):\n";
int ret = poll(fds, 1, 10000); // 等待 10 秒
if (ret == -1)
{
perror("poll 出错");
return 1;
}
else if (ret == 0)
{
std::cout << "超时,无输入\n";
}
else
{
if (fds[0].revents & POLLIN)
{
char buf[256];
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (n > 0)
{
buf[n] = '\0';
std::cout << "你输入了: " << buf;
}
}
}
}
3. 多 socket 回声服务器示例(收到什么返回什么)
- 编译方法:
g++ server_poll.c -o server_poll
- 服务器源码(可运行,默认绑定本机所有ip的输入端口)
- 使用方法:程序名 绑定端口
./server_poll 8888
// server_poll.c
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAX_CLIENTS 1024
#define BUF_SIZE 4096
int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "用法: %s <端口号>\n", argv[0]);
exit(1);
}
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0)
{
perror("socket");
exit(1);
}
// 地址端口复用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 待绑定的服务器地址
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(argv[1]));
// 绑定
if (bind(listenfd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
perror("bind");
exit(1);
}
// 监听
listen(listenfd, SOMAXCONN);
printf("服务器启动,监听端口 %s...\n", argv[1]);
struct pollfd fds[MAX_CLIENTS];
fds[0].fd = listenfd; // 记住,fds[0]属于服务器监听
fds[0].events = POLLIN; // listenfd绑定POLLIN事件(只管理POLLIN事件)
for (int i = 1; i < MAX_CLIENTS; i++)
fds[i].fd = -1;
while (1)
{
int ret = poll(fds, MAX_CLIENTS, -1); // 无限等待事件的到来
if (ret < 0)
{
perror("poll");
break;
}
// 新连接
if (fds[0].revents & POLLIN) // 监听fd(服务器的fd)来了事件是有了新连接
{
struct sockaddr_in cli;
socklen_t len = sizeof(cli);
int cfd = accept(listenfd, (struct sockaddr *)&cli, &len);
if (cfd >= 0)
{
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
printf("新连接:%s:%d fd=%d\n", ip, ntohs(cli.sin_port), cfd);
// 将新来的连接放在fds数组里保存
int i;
for (i = 1; i < MAX_CLIENTS; i++)
{
if (fds[i].fd == -1) // -1代表该位置为空
{
// 保存客户端fd和需要监听客户端fd的事件
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
if (i == MAX_CLIENTS)
{
printf("连接过多,拒绝新连接\n");
close(cfd);
}
}
}
// 客户端的fd来了事件
for (int i = 1; i < MAX_CLIENTS; i++)
{
int fd = fds[i].fd;
if (fd == -1)
continue;
if (fds[i].revents & (POLLERR | POLLHUP | POLLNVAL))
{
printf("关闭 fd=%d\n", fd);
close(fd);
fds[i].fd = -1;
continue;
}
if (fds[i].revents & POLLIN) // 客户端fd来了数据
{
char buf[BUF_SIZE];
memset(buf, 0, BUF_SIZE); // 每次清空接收buf
ssize_t n = read(fd, buf, sizeof(buf));
if (n <= 0)
{
printf("客户端 fd=%d 断开\n", fd);
close(fd);
fds[i].fd = -1;
}
else
{
printf("recv [%d]:%s", i, buf);
write(fd, buf, n); // 将接收到的buf里的数据返回给客户端
}
}
}
}
close(listenfd);
return 0;
}
4. 客户端多线程程序
- 编译方法:因为是多线程,所以需要使用-lpthread来链接相关库。
gcc client.c -lpthread -o client
- 客户端源码(可运行,程序里通过宏 #define SERVER_PORT 8888 指定了连接的目的端口)
- 使用方法:程序名
./client
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>
#define SERVER_IP "127.0.0.1" // 目标服务器 IP,可改为本机ipv4地址
#define SERVER_PORT 8888 // 目标服务器端口
#define THREAD_COUNT 5 // 线程数量
// 线程参数结构体
typedef struct
{
int thread_id; // 线程编号
char *server_ip; // 服务器 IP
int server_port; // 服务器端口
} thread_param_t;
// 线程函数:处理单个连接
void *client_thread(void *arg)
{
thread_param_t *param = (thread_param_t *)arg;
int sockfd;
struct sockaddr_in server_addr;
// 创建套接字
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket creation failed");
pthread_exit(NULL);
}
// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(param->server_port);
if (inet_pton(AF_INET, param->server_ip, &server_addr.sin_addr) <= 0)
{
perror("invalid address");
close(sockfd);
pthread_exit(NULL);
}
// 连接服务器
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
{
perror("connection failed");
close(sockfd);
pthread_exit(NULL);
}
printf("[Thread %d] Connected to server %s:%d\n", param->thread_id, param->server_ip, param->server_port);
// 通信示例:发送和接收数据
char buffer[1024] = {0};
const char *msg = "Hello from client";
for (int i = 0; i < 3; i++)
{ // 发送 3 次消息
snprintf(buffer, sizeof(buffer), "Thread %d: Message number %d\n", param->thread_id, i);
if (send(sockfd, buffer, strlen(buffer), 0) == -1)
{
perror("send failed");
break;
}
printf("[Thread %d] Sent: %s", param->thread_id, buffer);
// 接收服务器响应
ssize_t bytes_received = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (bytes_received <= 0)
{
printf("[Thread %d] Server disconnected\n", param->thread_id);
break;
}
buffer[bytes_received] = '\0';
printf("[Thread %d] Received: %s", param->thread_id, buffer);
}
// 关闭连接并退出线程
close(sockfd);
pthread_exit(NULL);
}
int main()
{
pthread_t threads[THREAD_COUNT];
thread_param_t params[THREAD_COUNT];
// 初始化线程参数
for (int i = 0; i < THREAD_COUNT; i++)
{
params[i].thread_id = i + 1;
params[i].server_ip = SERVER_IP;
params[i].server_port = SERVER_PORT;
}
// 创建线程
for (int i = 0; i < THREAD_COUNT; i++)
{
if (pthread_create(&threads[i], NULL, client_thread, ¶ms[i]) != 0)
{
perror("pthread_create failed");
exit(EXIT_FAILURE);
}
// 分离线程,自动回收资源
pthread_detach(threads[i]);
}
printf("All threads started. Press Ctrl+C to exit.\n");
// 主线程保持运行
while (1)
{
sleep(1);
}
return 0;
}
5. 服务器和客户端都运行后的结果
- 服务器结果
- recv [2] 代表当前接收的是fds数组里的第2个保存的fd的数据
可以将printf("recv [%d]:%s", i, buf);改成printf("recv [%d]:%s", fd, buf);就能知道客户端的fd
ddz@ddz:~/Videos/study/csdn/io/poll$ ./server_poll 8888
服务器启动,监听端口 8888...
新连接:127.0.0.1:34800 fd=4
新连接:127.0.0.1:34804 fd=5
新连接:127.0.0.1:34812 fd=6
recv [2]:Thread 2: Message number 0
新连接:127.0.0.1:34826 fd=7
recv [3]:Thread 3: Message number 0
新连接:127.0.0.1:34828 fd=8
recv [4]:Thread 4: Message number 0
recv [2]:Thread 2: Message number 1
recv [5]:Thread 5: Message number 0
recv [4]:Thread 4: Message number 1
recv [5]:Thread 5: Message number 1
recv [2]:Thread 2: Message number 2
recv [3]:Thread 3: Message number 1
recv [4]:Thread 4: Message number 2
recv [1]:Thread 1: Message number 0
客户端 fd=5 断开
recv [3]:Thread 3: Message number 2
客户端 fd=7 断开
客户端 fd=6 断开
recv [5]:Thread 5: Message number 2
recv [1]:Thread 1: Message number 1
客户端 fd=8 断开
recv [1]:Thread 1: Message number 2
客户端 fd=4 断开
- 客户端结果
- [Thread 2] 代表当前是第几个线程
- Sent:后面是发送的数据
- Received:后面是服务器发来的数据
ddz@ddz:~/Videos/study/csdn/io/poll$ ./client
All threads started. Press Ctrl+C to exit.
[Thread 2] Connected to server 127.0.0.1:8888
[Thread 3] Connected to server 127.0.0.1:8888
[Thread 2] Sent: Thread 2: Message number 0
[Thread 3] Sent: Thread 3: Message number 0
[Thread 4] Connected to server 127.0.0.1:8888
[Thread 5] Connected to server 127.0.0.1:8888
[Thread 2] Received: Thread 2: Message number 0
[Thread 2] Sent: Thread 2: Message number 1
[Thread 5] Sent: Thread 5: Message number 0
[Thread 4] Sent: Thread 4: Message number 0
[Thread 4] Received: Thread 4: Message number 0
[Thread 4] Sent: Thread 4: Message number 1
[Thread 5] Received: Thread 5: Message number 0
[Thread 3] Received: Thread 3: Message number 0
[Thread 4] Received: Thread 4: Message number 1
[Thread 4] Sent: Thread 4: Message number 2
[Thread 2] Received: Thread 2: Message number 1
[Thread 1] Connected to server 127.0.0.1:8888
[Thread 3] Sent: Thread 3: Message number 1
[Thread 3] Received: Thread 3: Message number 1
[Thread 2] Sent: Thread 2: Message number 2
[Thread 2] Received: Thread 2: Message number 2
[Thread 3] Sent: Thread 3: Message number 2
[Thread 4] Received: Thread 4: Message number 2
[Thread 3] Received: Thread 3: Message number 2
[Thread 5] Sent: Thread 5: Message number 1
[Thread 5] Received: Thread 5: Message number 1
[Thread 5] Sent: Thread 5: Message number 2
[Thread 1] Sent: Thread 1: Message number 0
[Thread 1] Received: Thread 1: Message number 0
[Thread 1] Sent: Thread 1: Message number 1
[Thread 5] Received: Thread 5: Message number 2
[Thread 1] Received: Thread 1: Message number 1
[Thread 1] Sent: Thread 1: Message number 2
[Thread 1] Received: Thread 1: Message number 2

73

被折叠的 条评论
为什么被折叠?



