文章目录
- [深入理解 `poll` 函数:详细解析与实际应用](#深入理解
poll函数:详细解析与实际应用) -
- [1. `poll` 函数简介](#1.
poll函数简介) - [2. `poll` 的底层机制与工作原理](#2.
poll的底层机制与工作原理) -
- [1. `poll` 与 `select` 的区别](#1.
poll与select的区别) - [2. 工作流程](#2. 工作流程)
- [1. `poll` 与 `select` 的区别](#1.
- [3. `poll` 使用场景](#3.
poll使用场景) - [4. `poll` 的典型代码示例](#4.
poll的典型代码示例) - [5. 常见问题解答](#5. 常见问题解答)
-
- [5.1 `poll` 和 `select` 的区别是什么?](#5.1
poll和select的区别是什么?) - [5.2 为什么使用 `pollfd` 数组时需要将未使用的套接字初始化为 `-1`?](#5.2 为什么使用
pollfd数组时需要将未使用的套接字初始化为-1?)
- [5.1 `poll` 和 `select` 的区别是什么?](#5.1
- 总结
- [1. `poll` 函数简介](#1.
深入理解 poll 函数:详细解析与实际应用
在网络编程中,处理多个客户端并发连接是一项常见且重要的任务。传统上,使用 select 函数来实现 多路复用。然而,随着对高性能和可扩展性的要求越来越高,poll 函数逐渐成为更常用的选择。本篇博客将详细介绍 poll 函数,并结合你遇到的难点,逐一解释其使用方法、底层机制及常见应用场景。
1. poll 函数简介
poll 是一种多路输入输出复用机制,常用于同时监听多个文件描述符的状态变化。与 select 函数类似,poll 通过阻塞等待多个文件描述符中的一个或多个事件发生,减少了在多客户端情况下需要处理的阻塞和轮询问题。
poll 函数原型
c
#include <poll.h>
int poll(struct pollfd fds, nfds_t nfds, int timeout);
参数解析:
fds:是一个指向 pollfd 结构体数组的指针。每个 pollfd 结构体表示一个需要监听的文件描述符。该结构体有以下几个字段:
fd:需要监听的文件描述符(通常是套接字)。
events:要监听的事件类型(如 POLLIN、POLLERR、POLLOUT 等)。
revents:实际发生的事件,poll 调用返回后,revents 会被填充,指示哪些事件已经发生。
nfds:fds 数组的大小,即需要监听的文件描述符的个数。
timeout:设置等待事件发生的超时时间,单位为毫秒。常用值有:
-1:无限等待,直到至少有一个事件发生。
0:立即返回(非阻塞模式)。
大于 0 的整数:指定最大等待时间,超时后返回。
返回值:
> 0:表示有文件描述符发生了事件,返回值是发生事件的文件描述符数量。
0:表示超时,没有事件发生。
< 0:表示出错,errno 会设置为相应的错误代码。
2. poll 的底层机制与工作原理
与 select 类似,poll 也是通过轮询来检查多个文件描述符的状态。但是,poll 在一些方面比 select 更加高效,尤其在文件描述符数量较多时。
1. poll 与 select 的区别
fd_set 结构:select 使用 fd_set 结构来管理文件描述符集,最大文件描述符的数量是固定的(通常为 1024)。而 poll 使用 pollfd 结构,它没有文件描述符数量的硬性限制,适合更大规模的文件描述符集合。
性能:由于 poll 不需要手动设置和清理 fd_set 结构(不像 select 那样需要每次修改),它的使用更简便。poll 通过直接操作 pollfd 数组,并在轮询时返回具体哪个文件描述符发生了事件。
超时机制:poll 支持精确的超时管理,可以精确设置等待时间,而 select 中只能粗略设置超时。
2. 工作流程
poll 会执行以下几个步骤:
- 初始化
pollfd数组:首先,设置一个pollfd数组,配置每个文件描述符的监听事件。 - 调用
poll()阻塞等待事件:poll会阻塞直到监听的文件描述符上有一个或多个事件发生,或者超时。 - 检查返回的事件:
poll返回后,检查每个文件描述符的revents字段,查看哪些文件描述符发生了预期的事件。
3. poll 使用场景
poll 适用于 高并发连接 的场景,尤其是以下几种情况:
- 监听多个客户端连接:
poll可以同时监听多个客户端套接字,避免在高并发情况下的阻塞问题。 - 网络服务器:在构建 高性能的网络服务器 时,
poll可以帮助处理大量并发连接,减少线程和进程的创建和销毁,提高系统的资源利用效率。 - I/O 多路复用:在需要监听多个输入输出流(如文件、网络套接字等)时,
poll提供了一个有效的解决方案。
4. poll 的典型代码示例
我们来看一个简单的 poll 示例,来展示如何使用它来处理多个客户端的连接。
c
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>
#define LISTEN_MAX 5
#define CLIENT_MAX 5
int main(void) {
char buff[1024];
int sock_fd, ret;
// 创建套接字
sock_fd = socket(PF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket failed");
exit(1);
}
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(8888);
if (bind(sock_fd, (struct sockaddr)&server_address, sizeof(server_address)) != 0) {
perror("bind failed");
exit(1);
}
listen(sock_fd, LISTEN_MAX);
// 设置 pollfd
struct pollfd fds[CLIENT_MAX + 1];
fds[0].fd = sock_fd;
fds[0].events = POLLIN;
// 初始化客户端套接字
for (int i = 1; i <= CLIENT_MAX; i++) {
fds[i].fd = -1; // 不监听,避免浪费资源
fds[i].events = 0;
}
while (1) {
ret = poll(fds, CLIENT_MAX + 1, -1);
if (ret < 0) {
perror("poll failed");
exit(1);
}
// 处理 server 套接字
if (fds[0].revents & POLLIN) {
struct sockaddr_in client_address;
socklen_t client_len = sizeof(client_address);
int client_fd = accept(sock_fd, (struct sockaddr)&client_address, &client_len);
if (client_fd < 0) {
perror("accept failed");
continue;
}
// 寻找空闲的槽位存储客户端套接字
for (int i = 1; i <= CLIENT_MAX; i++) {
if (fds[i].fd == -1) {
fds[i].fd = client_fd;
fds[i].events = POLLIN;
printf("New client connected: %d\n", client_fd);
break;
}
}
}
// 处理客户端请求
for (int i = 1; i <= CLIENT_MAX; i++) {
if (fds[i].fd != -1 && (fds[i].revents & POLLIN)) {
int client_fd = fds[i].fd;
ret = read(client_fd, buff, sizeof(buff));
if (ret <= 0) {
close(client_fd);
fds[i].fd = -1; // 清空客户端套接字
printf("Client %d disconnected\n", client_fd);
} else {
printf("Received from client %d: %s\n", client_fd, buff);
write(client_fd, buff, ret);
}
}
}
}
close(sock_fd);
return 0;
}
代码说明:
-
套接字初始化:首先创建套接字,绑定到指定的 IP 和端口。
-
pollfd数组初始化:为每个客户端套接字分配一个pollfd结构,监听可读事件(POLLIN)。 -
poll调用:在while循环中,调用poll()来等待客户端连接或数据。 -
处理事件:
服务端套接字:当服务端套接字可读时,接受新连接。
客户端套接字:当客户端套接字可读时,读取数据并回传。
5. 常见问题解答
5.1 poll 和 select 的区别是什么?
poll 和 select 都用于多路复用,但有以下几个区别:
select 的限制:select 使用一个最大文件描述符数(通常为 1024),无法处理更大的文件描述符集合。而 poll 没有这个限制,可以处理更多的文件描述符。
select 的性能问题:select 在文件描述符较多时,性能较差,因为每次调用时都需要检查所有的文件描述符。
poll 的优越性:poll 在大文件描述符集合中表现得更高效,因为它只需要处理传入的文件描述符数组。
5.2 为什么使用 pollfd 数组时需要将未使用的套接字初始化为 -1?
pollfd 数组中的每个元素都代表一个文件描述符。通过将未使用的套接字设置为 -1,可以确保 poll 只监听有效的文件描述符。这样做可以节省资源并避免监听无效的套接字。
总结
poll 是一种多路复用技术,适用于监听多个套接字事件。与 select 相比,poll 没有文件描述符数量的限制,且更高效。
poll 使用 pollfd 数组 来管理文件描述符,每个套接字的状态(如可读、错误等)都会存储在该结构中。
poll 适用于多客户端服务器,可以高效地处理多个客户端的并发请求。